fix: 前端页面架构重构初版
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npx vue-tsc --noEmit)",
|
||||
"Bash(timeout 60 npx vue-tsc:*)"
|
||||
"Bash(timeout 60 npx vue-tsc:*)",
|
||||
"Bash(npm run dev:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -549,6 +549,7 @@ onBeforeUnmount(() => {
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button
|
||||
v-if="false"
|
||||
:icon="FullScreen"
|
||||
circle
|
||||
plain
|
||||
|
||||
@@ -105,6 +105,8 @@ function bindWechat() {
|
||||
|
||||
<template>
|
||||
<div class="user-profile">
|
||||
<!-- 用户卡片 -->
|
||||
<el-card class="profile-card" shadow="hover">
|
||||
<!-- 顶部标题 -->
|
||||
<div class="header">
|
||||
<h2>
|
||||
@@ -112,9 +114,6 @@ function bindWechat() {
|
||||
个人信息
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- 用户卡片 -->
|
||||
<el-card class="profile-card" shadow="hover">
|
||||
<!-- 头像和基本信息区域 -->
|
||||
<div class="user-header-section">
|
||||
<!-- 头像区域 -->
|
||||
@@ -138,7 +137,9 @@ function bindWechat() {
|
||||
|
||||
<!-- 用户名称和状态 -->
|
||||
<div class="user-info-quick">
|
||||
<h3 class="user-name">{{ userNick }}</h3>
|
||||
<h3 class="user-name">
|
||||
{{ userNick }}
|
||||
</h3>
|
||||
<div class="user-tags">
|
||||
<el-tag v-if="userVipStatus" type="warning" effect="dark" size="large">
|
||||
<el-icon><Promotion /></el-icon>
|
||||
@@ -153,8 +154,12 @@ function bindWechat() {
|
||||
</div>
|
||||
<div class="user-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ formatDate(user.creationTime)?.split(' ')[0] || '-' }}</div>
|
||||
<div class="stat-label">注册时间</div>
|
||||
<div class="stat-value">
|
||||
{{ formatDate(user.creationTime)?.split(' ')[0] || '-' }}
|
||||
</div>
|
||||
<div class="stat-label">
|
||||
注册时间
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +176,9 @@ function bindWechat() {
|
||||
<el-icon><User /></el-icon>
|
||||
用户名
|
||||
</div>
|
||||
<div class="info-value">{{ user.userName || '-' }}</div>
|
||||
<div class="info-value">
|
||||
{{ user.userName || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 昵称 -->
|
||||
@@ -180,7 +187,9 @@ function bindWechat() {
|
||||
<el-icon><Postcard /></el-icon>
|
||||
昵称
|
||||
</div>
|
||||
<div class="info-value">{{ userNick }}</div>
|
||||
<div class="info-value">
|
||||
{{ userNick }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
@@ -215,7 +224,9 @@ function bindWechat() {
|
||||
<!-- 微信绑定 -->
|
||||
<div class="info-item full-width">
|
||||
<div class="info-label">
|
||||
<el-icon color="#07C160"><ChatDotRound /></el-icon>
|
||||
<el-icon color="#07C160">
|
||||
<ChatDotRound />
|
||||
</el-icon>
|
||||
微信绑定
|
||||
</div>
|
||||
<div class="info-value wechat-binding">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type LayoutType = | 'vertical' | 'blankPage';
|
||||
export type LayoutType = 'default' | 'vertical' | 'blankPage' | 'blankPage';
|
||||
|
||||
// 仿豆包折叠逻辑
|
||||
export type CollapseType
|
||||
@@ -25,7 +25,7 @@ export interface DesignConfigState {
|
||||
// 是否折叠菜单
|
||||
isCollapse: boolean;
|
||||
// 安全区是否被悬停
|
||||
isSafeAreaHover: boolean;
|
||||
isCollapseConversationList: boolean;
|
||||
// 跟踪是否首次激活悬停
|
||||
hasActivatedHover: boolean;
|
||||
}
|
||||
@@ -65,15 +65,13 @@ const design: DesignConfigState = {
|
||||
// 需要自定义路由动画可以把 Main 组件样式代码注释放开,从新对话切换到带id的路由时,会执行这个动画样式
|
||||
pageAnimateType: 'zoom-fade',
|
||||
// 布局模式 (纵向:vertical | ... | 自己定义)
|
||||
layout: 'vertical',
|
||||
layout: 'default',
|
||||
// 折叠类型
|
||||
collapseType: 'followSystem',
|
||||
// 是否折叠菜单
|
||||
// 是否折叠对话记录菜单
|
||||
isCollapse: false,
|
||||
// 安全区是否被悬停
|
||||
isSafeAreaHover: false,
|
||||
// 跟踪是否首次激活悬停
|
||||
hasActivatedHover: false,
|
||||
// 是否折叠对话记录菜单
|
||||
isCollapseConversationList: false,
|
||||
};
|
||||
|
||||
export default design;
|
||||
|
||||
72
Yi.Ai.Vue3/src/hooks/useResponsive.ts
Normal file
72
Yi.Ai.Vue3/src/hooks/useResponsive.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useBreakpoints, useWindowSize } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 断点定义
|
||||
export const breakpoints = {
|
||||
xs: 0, // 手机竖屏 < 640px
|
||||
sm: 640, // 手机横屏 ≥ 640px
|
||||
md: 768, // 平板 ≥ 768px
|
||||
lg: 1024, // 小桌面 ≥ 1024px
|
||||
xl: 1280, // 桌面 ≥ 1280px
|
||||
xxl: 1536, // 大桌面 ≥ 1536px
|
||||
};
|
||||
|
||||
export function useResponsive() {
|
||||
const bp = useBreakpoints(breakpoints);
|
||||
|
||||
// 设备类型判断
|
||||
const isMobile = bp.smaller('md'); // < 768px
|
||||
const isTablet = bp.between('md', 'lg'); // 768px - 1024px
|
||||
const isDesktop = bp.greaterOrEqual('lg'); // ≥ 1024px
|
||||
|
||||
// 精确断点
|
||||
const isXs = bp.smaller('sm'); // < 640px
|
||||
const isSm = bp.between('sm', 'md'); // 640px - 768px
|
||||
const isMd = bp.between('md', 'lg'); // 768px - 1024px
|
||||
const isLg = bp.between('lg', 'xl'); // 1024px - 1280px
|
||||
const isXl = bp.between('xl', 'xxl'); // 1280px - 1536px
|
||||
const isXxl = bp.greater('xxl'); // > 1536px
|
||||
|
||||
// 监听窗口变化
|
||||
const { width, height } = useWindowSize();
|
||||
|
||||
// 方向检测
|
||||
const isPortrait = computed(() => height.value > width.value);
|
||||
const isLandscape = computed(() => width.value > height.value);
|
||||
|
||||
// 当前断点名称
|
||||
const currentBreakpoint = computed(() => {
|
||||
if (isXs.value) return 'xs';
|
||||
if (isSm.value) return 'sm';
|
||||
if (isMd.value) return 'md';
|
||||
if (isLg.value) return 'lg';
|
||||
if (isXl.value) return 'xl';
|
||||
return 'xxl';
|
||||
});
|
||||
|
||||
return {
|
||||
// 设备类型
|
||||
isMobile,
|
||||
isTablet,
|
||||
isDesktop,
|
||||
|
||||
// 精确断点
|
||||
isXs,
|
||||
isSm,
|
||||
isMd,
|
||||
isLg,
|
||||
isXl,
|
||||
isXxl,
|
||||
|
||||
// 尺寸
|
||||
width,
|
||||
height,
|
||||
|
||||
// 方向
|
||||
isPortrait,
|
||||
isLandscape,
|
||||
|
||||
// 当前断点名称
|
||||
currentBreakpoint,
|
||||
};
|
||||
}
|
||||
@@ -19,8 +19,9 @@ export function useWindowWidthObserver(
|
||||
const isAboveThreshold = ref(false);
|
||||
const thresholdRef = ref(threshold);
|
||||
let prevIsAbove = false; // 记录上一次状态,避免重复触发
|
||||
|
||||
// 待定 待梳理 1227
|
||||
// 默认逻辑:修改全局折叠状态
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const updateCollapseState = (isAbove: boolean) => {
|
||||
// 判断当前的折叠状态
|
||||
switch (designStore.collapseType) {
|
||||
@@ -70,7 +71,7 @@ export function useWindowWidthObserver(
|
||||
onChange(newIsAbove);
|
||||
}
|
||||
else {
|
||||
updateCollapseState(newIsAbove);
|
||||
// updateCollapseState(newIsAbove);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
35
Yi.Ai.Vue3/src/layouts/LayoutDefault/index.vue
Normal file
35
Yi.Ai.Vue3/src/layouts/LayoutDefault/index.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import SystemAnnouncementDialog from '@/components/SystemAnnouncementDialog/index.vue';
|
||||
import Header from '@/layouts/components/Header/index.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-header class="layout-header">
|
||||
<Header />
|
||||
</el-header>
|
||||
<el-container class="layout-container-main">
|
||||
<router-view />
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<!-- 系统公告弹窗 -->
|
||||
<SystemAnnouncementDialog />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--color-gray-100);
|
||||
.layout-header {
|
||||
padding: 0;
|
||||
border-bottom: var(--header-border) ;
|
||||
}
|
||||
.layout-container-main {
|
||||
height: calc(100vh - var(--header-container-default-height));
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,288 @@
|
||||
<!-- 手机端布局 -->
|
||||
<script setup></script>
|
||||
<!-- 移动端布局 -->
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 侧边栏抽屉状态
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
// 底部导航菜单
|
||||
const bottomMenus = [
|
||||
{
|
||||
key: 'chat',
|
||||
label: '对话',
|
||||
icon: 'ChatDotRound',
|
||||
path: '/chat/conversation',
|
||||
},
|
||||
{
|
||||
key: 'image',
|
||||
label: '图片',
|
||||
icon: 'Picture',
|
||||
path: '/chat/image',
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
label: '视频',
|
||||
icon: 'VideoCamera',
|
||||
path: '/chat/video',
|
||||
},
|
||||
{
|
||||
key: 'console',
|
||||
label: '我的',
|
||||
icon: 'User',
|
||||
path: '/console',
|
||||
},
|
||||
];
|
||||
|
||||
// 侧边栏菜单
|
||||
const sidebarMenus = [
|
||||
{
|
||||
key: 'model-library',
|
||||
label: '模型库',
|
||||
icon: 'Box',
|
||||
path: '/model-library',
|
||||
},
|
||||
{
|
||||
key: 'pricing',
|
||||
label: '购买',
|
||||
icon: 'ShoppingCart',
|
||||
path: '/pricing',
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
label: '退出登录',
|
||||
icon: 'SwitchButton',
|
||||
action: 'logout',
|
||||
},
|
||||
];
|
||||
|
||||
// 当前路由
|
||||
const currentPath = computed(() => router.currentRoute.value.path);
|
||||
|
||||
// 当前激活的底部菜单
|
||||
const activeBottomMenu = computed(() => {
|
||||
const path = currentPath.value;
|
||||
if (path.includes('/chat/conversation')) return 'chat';
|
||||
if (path.includes('/chat/image')) return 'image';
|
||||
if (path.includes('/chat/video')) return 'video';
|
||||
if (path.includes('/console')) return 'console';
|
||||
return 'chat';
|
||||
});
|
||||
|
||||
// 打开抽屉
|
||||
function openDrawer() {
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
// 底部菜单点击
|
||||
function handleBottomMenuClick(menu: typeof bottomMenus[0]) {
|
||||
router.push(menu.path);
|
||||
}
|
||||
|
||||
// 侧边栏菜单点击
|
||||
function handleSidebarMenuClick(menu: typeof sidebarMenus[0]) {
|
||||
if (menu.action === 'logout') {
|
||||
userStore.logout();
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
else if (menu.path) {
|
||||
router.push(menu.path);
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
<div class="mobile-layout">
|
||||
<!-- 顶部栏 -->
|
||||
<div class="mobile-header">
|
||||
<el-button circle @click="openDrawer">
|
||||
<el-icon><i-ep-menu /></el-icon>
|
||||
</el-button>
|
||||
<div class="header-title">意心AI</div>
|
||||
<div class="header-avatar">
|
||||
<el-avatar v-if="userStore.userInfo" :size="32" :src="userStore.userInfo.avatar">
|
||||
{{ userStore.userInfo.name?.charAt(0) }}
|
||||
</el-avatar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="mobile-main">
|
||||
<router-view />
|
||||
</div>
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<div class="mobile-bottom-nav">
|
||||
<div
|
||||
v-for="menu in bottomMenus"
|
||||
:key="menu.key"
|
||||
class="nav-item"
|
||||
:class="{ active: activeBottomMenu === menu.key }"
|
||||
@click="handleBottomMenuClick(menu)"
|
||||
>
|
||||
<el-icon class="nav-icon">
|
||||
<component :is="`i-ep-${menu.icon}`" />
|
||||
</el-icon>
|
||||
<div class="nav-label">
|
||||
{{ menu.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏抽屉 -->
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
title="菜单"
|
||||
direction="ltr"
|
||||
size="280px"
|
||||
>
|
||||
<!-- 用户信息 -->
|
||||
<div v-if="userStore.userInfo" class="drawer-user">
|
||||
<el-avatar :size="60" :src="userStore.userInfo.avatar">
|
||||
{{ userStore.userInfo.name?.charAt(0) }}
|
||||
</el-avatar>
|
||||
<div class="user-info">
|
||||
<div class="user-name">{{ userStore.userInfo.name }}</div>
|
||||
<div class="user-email">{{ userStore.userInfo.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<el-menu class="drawer-menu">
|
||||
<el-menu-item
|
||||
v-for="menu in sidebarMenus"
|
||||
:key="menu.key"
|
||||
@click="handleSidebarMenuClick(menu)"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="`i-ep-${menu.icon}`" />
|
||||
</el-icon>
|
||||
<span>{{ menu.label }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.mobile-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 16px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.header-avatar {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mobile-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.mobile-bottom-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: 56px;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background-color: var(--el-bg-color);
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
color: var(--el-color-primary);
|
||||
|
||||
.nav-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 2px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// 抽屉样式
|
||||
.drawer-user {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 16px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-top: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.drawer-menu {
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
<script setup lang="ts">
|
||||
import SystemAnnouncementDialog from '@/components/SystemAnnouncementDialog/index.vue';
|
||||
import { useSafeArea } from '@/hooks/useSafeArea';
|
||||
import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver';
|
||||
import Aside from '@/layouts/components/Aside/index.vue';
|
||||
import Header from '@/layouts/components/Header/index.vue';
|
||||
import Main from '@/layouts/components/Main/index.vue';
|
||||
import { useAnnouncementStore, useDesignStore } from '@/stores';
|
||||
@@ -25,7 +23,7 @@ useSafeArea({
|
||||
});
|
||||
|
||||
/** 监听窗口大小变化,折叠侧边栏 */
|
||||
useWindowWidthObserver();
|
||||
// useWindowWidthObserver();
|
||||
|
||||
// 应用加载时检查是否需要显示公告弹窗
|
||||
onMounted(() => {
|
||||
@@ -43,7 +41,7 @@ onMounted(() => {
|
||||
<Header />
|
||||
</el-header>
|
||||
<el-container class="layout-container-main">
|
||||
<Aside />
|
||||
<!-- <Aside /> -->
|
||||
<el-main class="layout-main">
|
||||
<!-- 路由页面 -->
|
||||
<Main />
|
||||
@@ -55,29 +53,29 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
.layout-header {
|
||||
padding: 0;
|
||||
}
|
||||
.layout-main {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
.layout-container-main {
|
||||
margin-left: var(--sidebar-left-container-default-width, 0);
|
||||
transition: margin-left 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/** 去除菜单右侧边框 */
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
.layout-scrollbar {
|
||||
width: 100%;
|
||||
}
|
||||
//.layout-container {
|
||||
// position: relative;
|
||||
// width: 100%;
|
||||
// height: 100vh;
|
||||
// overflow: hidden;
|
||||
// .layout-header {
|
||||
// padding: 0;
|
||||
// }
|
||||
// .layout-main {
|
||||
// height: 100%;
|
||||
// padding: 0;
|
||||
// }
|
||||
// .layout-container-main {
|
||||
// margin-left: var(--sidebar-left-container-default-width, 0);
|
||||
// transition: margin-left 0.3s ease;
|
||||
// }
|
||||
//}
|
||||
//
|
||||
///** 去除菜单右侧边框 */
|
||||
//.el-menu {
|
||||
// border-right: none;
|
||||
//}
|
||||
//.layout-scrollbar {
|
||||
// width: 100%;
|
||||
//}
|
||||
</style>
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
<!-- Aside 侧边栏 -->
|
||||
<script setup lang="ts">
|
||||
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations';
|
||||
import type { ChatSessionVo } from '@/api/session/types';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { get_session } from '@/api';
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import Collapse from '@/layouts/components/Header/components/Collapse.vue';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
const conversationsList = computed(() => sessionStore.sessionList);
|
||||
const loadMoreLoading = computed(() => sessionStore.isLoadingMore);
|
||||
const active = ref<string | undefined>();
|
||||
|
||||
onMounted(async () => {
|
||||
// 获取会话列表
|
||||
await sessionStore.requestSessionList();
|
||||
// 高亮最新会话
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
// 通过 ID 查询详情,设置当前会话 (因为有分页)
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sessionStore.currentSession,
|
||||
(newValue) => {
|
||||
active.value = newValue ? `${newValue.id}` : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
// 创建会话
|
||||
function handleCreatChat() {
|
||||
// 创建会话, 跳转到默认聊天
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function handleChange(item: ConversationItem<ChatSessionVo>) {
|
||||
sessionStore.setCurrentSession(item);
|
||||
router.replace({
|
||||
name: 'chatWithId',
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 处理组件触发的加载更多事件
|
||||
async function handleLoadMore() {
|
||||
if (!sessionStore.hasMore)
|
||||
return; // 无更多数据时不加载
|
||||
await sessionStore.loadMoreSessions();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo>) {
|
||||
switch (command) {
|
||||
case 'delete':
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
// 删除会话
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
// 如果删除当前会话 返回到默认页
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: item.sessionTitle, // 设置默认值
|
||||
autofocus: false,
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: item.id!,
|
||||
sessionTitle: value,
|
||||
sessionContent: item.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
// 如果是当前会话,则更新当前选中会话信息
|
||||
if (sessionStore.currentSession?.id === item.id) {
|
||||
sessionStore.setCurrentSession({
|
||||
...item,
|
||||
sessionTitle: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="aside-container"
|
||||
:class="{
|
||||
'aside-container-suspended': designStore.isSafeAreaHover,
|
||||
'aside-container-collapse': designStore.isCollapse,
|
||||
// 折叠且未激活悬停时添加 no-delay 类
|
||||
'no-delay': designStore.isCollapse && !designStore.hasActivatedHover,
|
||||
}"
|
||||
>
|
||||
<div class="aside-wrapper">
|
||||
<div v-if="!designStore.isCollapse" class="aside-header">
|
||||
<div class="flex items-center gap-8px hover:cursor-pointer" @click="handleCreatChat">
|
||||
<el-image :src="logo" alt="logo" fit="cover" class="logo-img" />
|
||||
<span class="logo-text max-w-150px text-overflow">意心AI</span>
|
||||
</div>
|
||||
<Collapse class="ml-auto" />
|
||||
</div>
|
||||
|
||||
<div class="aside-body">
|
||||
<div class="creat-chat-btn-wrapper">
|
||||
<div class="creat-chat-btn" @click="handleCreatChat">
|
||||
<el-icon class="add-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span class="creat-chat-text">新对话</span>
|
||||
<SvgIcon name="ctrl+k" size="37" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="aside-content">
|
||||
<div v-if="conversationsList.length > 0" class="conversations-wrap overflow-hidden">
|
||||
<Conversations
|
||||
v-model:active="active"
|
||||
:items="conversationsList"
|
||||
:label-max-width="200"
|
||||
:show-tooltip="true"
|
||||
:tooltip-offset="60"
|
||||
show-built-in-menu
|
||||
groupable
|
||||
row-key="id"
|
||||
label-key="sessionTitle"
|
||||
tooltip-placement="right"
|
||||
:load-more="handleLoadMore"
|
||||
:load-more-loading="loadMoreLoading"
|
||||
:items-style="{
|
||||
marginLeft: '8px',
|
||||
userSelect: 'none',
|
||||
borderRadius: '10px',
|
||||
padding: '8px 12px',
|
||||
}"
|
||||
:items-active-style="{
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
}"
|
||||
:items-hover-style="{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
}"
|
||||
@menu-command="handleMenuCommand"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-else class="h-full flex-center" description="暂无对话记录" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 基础样式
|
||||
.aside-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
// z-index: 11;
|
||||
width: var(--sidebar-default-width);
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-right: 0.5px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
.aside-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
// 侧边栏头部样式
|
||||
.aside-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin: 10px 12px 0;
|
||||
.logo-img {
|
||||
box-sizing: border-box;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
border-radius: 50%;
|
||||
img {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
transform: skewX(-2deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏内容样式
|
||||
.aside-body {
|
||||
.creat-chat-btn-wrapper {
|
||||
padding: 0 12px;
|
||||
.creat-chat-btn {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 8px 6px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
color: #0057ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgb(0 87 255 / 6%);
|
||||
border: 1px solid rgb(0 102 255 / 15%);
|
||||
border-radius: 12px;
|
||||
&:hover {
|
||||
background-color: rgb(0 87 255 / 12%);
|
||||
}
|
||||
.creat-chat-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
.add-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.svg-icon {
|
||||
height: 24px;
|
||||
margin-left: auto;
|
||||
color: rgb(0 87 255 / 30%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.aside-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
||||
// 会话列表高度-基础样式
|
||||
.conversations-wrap {
|
||||
height: calc(100vh - 110px);
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠样式
|
||||
.aside-container-collapse {
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
// z-index: 22;
|
||||
height: auto;
|
||||
max-height: calc(100% - 110px);
|
||||
padding-bottom: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
/* 禁用悬停事件 */
|
||||
pointer-events: none;
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 15px;
|
||||
box-shadow:
|
||||
0 10px 20px 0 rgb(0 0 0 / 10%),
|
||||
0 0 1px 0 rgb(0 0 0 / 15%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease 0.3s, transform 0.3s ease 0.3s;
|
||||
|
||||
// 指定样式过渡
|
||||
|
||||
// 向左偏移一个宽度
|
||||
transform: translateX(-100%);
|
||||
|
||||
/* 新增:未激活悬停时覆盖延迟 */
|
||||
&.no-delay {
|
||||
transition-delay: 0s, 0s;
|
||||
}
|
||||
}
|
||||
|
||||
// 悬停样式
|
||||
.aside-container-collapse:hover,
|
||||
.aside-container-collapse.aside-container-suspended {
|
||||
height: auto;
|
||||
max-height: calc(100% - 110px);
|
||||
padding-bottom: 12px;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 15px;
|
||||
box-shadow:
|
||||
0 10px 20px 0 rgb(0 0 0 / 10%),
|
||||
0 0 1px 0 rgb(0 0 0 / 15%);
|
||||
|
||||
// 直接在这里写悬停时的样式(与 aside-container-suspended 一致)
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease 0s, transform 0.3s ease 0s;
|
||||
|
||||
// 过渡动画沿用原有设置
|
||||
transform: translateX(15px);
|
||||
|
||||
// 会话列表高度-悬停样式
|
||||
.conversations-wrap {
|
||||
height: calc(100vh - 155px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 样式穿透
|
||||
:deep() {
|
||||
// 会话列表背景色
|
||||
.conversations-list {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
// 群组标题样式 和 侧边栏菜单背景色一致
|
||||
.conversation-group-title {
|
||||
padding-left: 12px !important;
|
||||
background-color: var(--sidebar-background-color) !important;
|
||||
}
|
||||
.conversation-group .active-sticky
|
||||
{
|
||||
z-index: 0 ;
|
||||
}
|
||||
.conversation-group .sticky-title{
|
||||
z-index: 0 ;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
918
Yi.Ai.Vue3/src/layouts/components/ChatAside/index.vue
Normal file
918
Yi.Ai.Vue3/src/layouts/components/ChatAside/index.vue
Normal file
@@ -0,0 +1,918 @@
|
||||
<script setup lang="ts">
|
||||
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations';
|
||||
import type { ChatSessionVo } from '@/api/session/types';
|
||||
import { ChatLineSquare, Expand, Fold, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { get_session } from '@/api';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
const conversationsList = computed(() => sessionStore.sessionList);
|
||||
const loadMoreLoading = computed(() => sessionStore.isLoadingMore);
|
||||
const active = ref<string | undefined>();
|
||||
const isCollapsed = computed(() => designStore.isCollapseConversationList);
|
||||
|
||||
onMounted(async () => {
|
||||
await sessionStore.requestSessionList();
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sessionStore.currentSession,
|
||||
(newValue) => {
|
||||
active.value = newValue ? `${newValue.id}` : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
// 创建会话
|
||||
function handleCreatChat() {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function handleChange(item: ConversationItem<ChatSessionVo>) {
|
||||
sessionStore.setCurrentSession(item);
|
||||
router.replace({
|
||||
name: 'chatConversationWithId',
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 处理组件触发的加载更多事件
|
||||
async function handleLoadMore() {
|
||||
if (!sessionStore.hasMore)
|
||||
return;
|
||||
await sessionStore.loadMoreSessions();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo>) {
|
||||
switch (command) {
|
||||
case 'delete':
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: item.sessionTitle,
|
||||
autofocus: false,
|
||||
inputValidator: (value) => {
|
||||
return !!value;
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: item.id!,
|
||||
sessionTitle: value,
|
||||
sessionContent: item.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
if (sessionStore.currentSession?.id === item.id) {
|
||||
sessionStore.setCurrentSession({
|
||||
...item,
|
||||
sessionTitle: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠/展开侧边栏
|
||||
function toggleSidebar() {
|
||||
// designStore.setIsCollapseConversationList(!designStore.isCollapseConversationList);
|
||||
}
|
||||
|
||||
// 点击logo创建新会话(仅在折叠状态)
|
||||
function handleLogoClick() {
|
||||
if (isCollapsed.value) {
|
||||
handleCreatChat();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理右键菜单(折叠状态下使用)
|
||||
function handleContextMenu(event: MouseEvent, item: ConversationItem<ChatSessionVo>) {
|
||||
event.preventDefault();
|
||||
// 在折叠状态下触发删除确认
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
}
|
||||
|
||||
// 折叠状态下点击更多按钮
|
||||
function handleCollapsedMenuClick(event: MouseEvent, item: ConversationItem<ChatSessionVo>) {
|
||||
event.stopPropagation();
|
||||
|
||||
// 创建一个简单的菜单
|
||||
ElMessageBox({
|
||||
title: '对话操作',
|
||||
message: `
|
||||
<div style="padding: 8px 0;">
|
||||
<div class="menu-item" data-action="rename" style="padding: 8px 12px; cursor: pointer; border-radius: 4px; margin: 4px 0;">
|
||||
<span style="font-size: 14px;">重命名</span>
|
||||
</div>
|
||||
<div class="menu-item" data-action="delete" style="padding: 8px 12px; cursor: pointer; border-radius: 4px; margin: 4px 0; color: #f56c6c;">
|
||||
<span style="font-size: 14px;">删除对话</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
showConfirmButton: false,
|
||||
showCancelButton: false,
|
||||
dangerouslyUseHTMLString: true,
|
||||
customClass: 'collapsed-menu-dialog',
|
||||
closeOnClickModal: true,
|
||||
closeOnPressEscape: true,
|
||||
}).then(() => {
|
||||
// 对话框关闭
|
||||
}).catch(() => {
|
||||
// 对话框关闭
|
||||
});
|
||||
|
||||
// 添加菜单项点击事件
|
||||
nextTick(() => {
|
||||
const menuItems = document.querySelectorAll('.menu-item');
|
||||
menuItems.forEach((itemEl) => {
|
||||
itemEl.addEventListener('click', (e) => {
|
||||
const action = (e.currentTarget as HTMLElement).dataset.action;
|
||||
if (action === 'delete') {
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
}
|
||||
else if (action === 'rename') {
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: item.sessionTitle,
|
||||
autofocus: false,
|
||||
inputValidator: (value) => {
|
||||
return !!value;
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: item.id!,
|
||||
sessionTitle: value,
|
||||
sessionContent: item.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
if (sessionStore.currentSession?.id === item.id) {
|
||||
sessionStore.setCurrentSession({
|
||||
...item,
|
||||
sessionTitle: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭菜单对话框
|
||||
document.querySelector('.collapsed-menu-dialog .el-message-box__headerbtn')?.dispatchEvent(new Event('click'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="aside-container"
|
||||
:class="{ 'aside-collapsed': isCollapsed }"
|
||||
>
|
||||
<div class="aside-wrapper">
|
||||
<!-- 头部 -->
|
||||
<div class="aside-header">
|
||||
<!-- 展开状态显示logo和标题 -->
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
class="header-content-expanded flex items-center gap-8px hover:cursor-pointer"
|
||||
@click="handleCreatChat"
|
||||
>
|
||||
<span class="logo-text max-w-150px text-overflow">会话</span>
|
||||
</div>
|
||||
|
||||
<!-- 折叠状态只显示logo -->
|
||||
<div
|
||||
v-else
|
||||
class="header-content-collapsed flex items-center justify-center hover:cursor-pointer"
|
||||
@click="handleLogoClick"
|
||||
>
|
||||
<el-icon size="20">
|
||||
<ChatLineSquare />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 折叠按钮 -->
|
||||
<el-tooltip
|
||||
:content="isCollapsed ? '展开侧边栏' : '折叠侧边栏'"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button
|
||||
class="collapse-btn"
|
||||
type="text"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<el-icon v-if="isCollapsed">
|
||||
<Expand />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Fold />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="aside-body">
|
||||
<!-- 创建会话按钮 -->
|
||||
<div class="creat-chat-btn-wrapper">
|
||||
<div
|
||||
class="creat-chat-btn"
|
||||
:class="{ 'creat-chat-btn-collapsed': isCollapsed }"
|
||||
@click="handleCreatChat"
|
||||
>
|
||||
<el-icon class="add-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span v-if="!isCollapsed" class="creat-chat-text">
|
||||
新对话
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<div class="aside-content">
|
||||
<div v-if="conversationsList.length > 0" class="conversations-wrap">
|
||||
<Conversations
|
||||
v-model:active="active"
|
||||
:items="conversationsList"
|
||||
:label-max-width="isCollapsed ? 0 : 140"
|
||||
:show-tooltip="!isCollapsed"
|
||||
:tooltip-offset="60"
|
||||
show-built-in-menu
|
||||
groupable
|
||||
row-key="id"
|
||||
label-key="sessionTitle"
|
||||
:tooltip-placement="isCollapsed ? 'right-start' : 'right'"
|
||||
:load-more="handleLoadMore"
|
||||
:load-more-loading="loadMoreLoading"
|
||||
:items-style="{
|
||||
marginLeft: '8px',
|
||||
marginRight: '8px',
|
||||
userSelect: 'none',
|
||||
borderRadius: isCollapsed ? '12px' : '10px',
|
||||
padding: isCollapsed ? '12px 8px' : '8px 12px',
|
||||
justifyContent: isCollapsed ? 'center' : 'space-between',
|
||||
width: isCollapsed ? '64px' : 'auto',
|
||||
height: isCollapsed ? '64px' : 'auto',
|
||||
minHeight: '48px',
|
||||
flexDirection: isCollapsed ? 'column' : 'row',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
:items-active-style="{
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
}"
|
||||
:items-hover-style="{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
}"
|
||||
@menu-command="handleMenuCommand"
|
||||
@change="handleChange"
|
||||
@contextmenu="handleContextMenu"
|
||||
>
|
||||
<!-- 自定义折叠状态下的会话项内容 -->
|
||||
<template #default="{ item }">
|
||||
<div class="conversation-item-content">
|
||||
<div v-if="isCollapsed" class="collapsed-item">
|
||||
<div
|
||||
class="avatar-circle"
|
||||
@click="handleChange(item)"
|
||||
@contextmenu="(e) => handleContextMenu(e, item)"
|
||||
>
|
||||
{{ item.sessionTitle?.charAt(0) || 'A' }}
|
||||
</div>
|
||||
<div v-if="item.unreadCount" class="unread-indicator">
|
||||
{{ item.unreadCount > 99 ? '99+' : item.unreadCount }}
|
||||
</div>
|
||||
<!-- 折叠状态下的更多操作按钮 -->
|
||||
<div
|
||||
class="collapsed-menu-trigger"
|
||||
@click.stop="(e) => handleCollapsedMenuClick(e, item)"
|
||||
@contextmenu.stop="(e) => handleContextMenu(e, item)"
|
||||
>
|
||||
<el-icon size="14">
|
||||
<MoreFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="expanded-item">
|
||||
<div class="conversation-info">
|
||||
<div class="conversation-title">
|
||||
{{ item.sessionTitle }}
|
||||
</div>
|
||||
<div v-if="item.sessionContent" class="conversation-preview">
|
||||
{{ item.sessionContent.substring(0, 30) }}{{ item.sessionContent.length > 30 ? '...' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Conversations>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-else
|
||||
class="h-full flex-center"
|
||||
:description="isCollapsed ? '' : '暂无对话记录'"
|
||||
>
|
||||
<template #description>
|
||||
<span v-if="!isCollapsed">暂无对话记录</span>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 基础样式
|
||||
.aside-container {
|
||||
height: 100%;
|
||||
border-right: 0.5px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--sidebar-background-color, #f9fafb);
|
||||
|
||||
// 展开状态 - 240px
|
||||
&:not(.aside-collapsed) {
|
||||
width: 240px;
|
||||
|
||||
.aside-wrapper {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠状态 - 100px
|
||||
&.aside-collapsed {
|
||||
width: 100px;
|
||||
|
||||
.aside-wrapper {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// 头部样式
|
||||
.aside-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
//background-color: var(--sidebar-header-bg, #ffffff);
|
||||
|
||||
.header-content-expanded {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-content-collapsed {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
transform: skewX(-2deg);
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color-light);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.aside-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.creat-chat-btn-wrapper {
|
||||
padding: 12px 8px 4px;
|
||||
|
||||
.creat-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
color: #0057ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgb(0 87 255 / 6%);
|
||||
border: 1px solid rgb(0 102 255 / 15%);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(0 87 255 / 12%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.creat-chat-btn-collapsed {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.creat-chat-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
margin-left: 6px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.conversations-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.conversation-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.collapsed-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.avatar-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collapsed-menu-trigger {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 2;
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .collapsed-menu-trigger {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
.conversation-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
.conversation-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.conversation-preview {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 样式穿透 - 重点修复溢出问题
|
||||
:deep() {
|
||||
.conversations-list {
|
||||
background-color: transparent !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.conversation-group-title {
|
||||
padding-left: 12px !important;
|
||||
background-color: transparent !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.title-text {
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
transition: all 0.3s ease;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
|
||||
&-inner {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
&-content {
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
max-width: calc(100% - 32px) !important;
|
||||
overflow: hidden !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
// 确保操作按钮区域在展开状态下正常显示
|
||||
&-actions {
|
||||
flex-shrink: 0 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.el-button {
|
||||
transition: all 0.2s ease;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
padding: 0 !important;
|
||||
margin-left: 4px !important;
|
||||
flex-shrink: 0 !important;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠状态样式
|
||||
.aside-collapsed {
|
||||
.conversation-group-title {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
justify-content: center !important;
|
||||
padding: 12px 8px !important;
|
||||
height: 64px !important;
|
||||
min-height: 64px !important;
|
||||
|
||||
&-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
// 折叠状态下隐藏默认操作按钮,使用自定义的
|
||||
display: none !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
&-content {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 展开状态样式
|
||||
&:not(.aside-collapsed) {
|
||||
.conversation-item {
|
||||
&-actions {
|
||||
display: flex !important;
|
||||
opacity: 1 !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义对话框样式
|
||||
:deep(.collapsed-menu-dialog) {
|
||||
.el-message-box {
|
||||
width: 160px !important;
|
||||
padding: 12px !important;
|
||||
|
||||
&__header {
|
||||
padding: 0 0 8px 0 !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 14px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
&__headerbtn {
|
||||
top: 8px !important;
|
||||
right: 8px !important;
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.aside-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
width: 280px !important;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
&.aside-collapsed {
|
||||
transform: translateX(-100%);
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
&:not(.aside-collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-wrapper {
|
||||
width: 280px !important;
|
||||
|
||||
.aside-collapsed & {
|
||||
width: 100px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端遮罩层
|
||||
.aside-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -30,40 +30,40 @@ const popoverRef = ref();
|
||||
// 弹出面板内容
|
||||
const popoverList = ref([
|
||||
|
||||
{
|
||||
key: '5',
|
||||
title: '控制台',
|
||||
icon: 'settings-4-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
key: '7',
|
||||
title: '公告',
|
||||
icon: 'notification-fill',
|
||||
},
|
||||
{
|
||||
key: '8',
|
||||
title: '模型库',
|
||||
icon: 'apps-fill',
|
||||
},
|
||||
{
|
||||
key: '9',
|
||||
title: '文档',
|
||||
icon: 'book-fill',
|
||||
},
|
||||
|
||||
{
|
||||
key: '6',
|
||||
title: '新手引导',
|
||||
icon: 'dashboard-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
// {
|
||||
// key: '5',
|
||||
// title: '控制台',
|
||||
// icon: 'settings-4-fill',
|
||||
// },
|
||||
// {
|
||||
// key: '3',
|
||||
// divider: true,
|
||||
// },
|
||||
// {
|
||||
// key: '7',
|
||||
// title: '公告',
|
||||
// icon: 'notification-fill',
|
||||
// },
|
||||
// {
|
||||
// key: '8',
|
||||
// title: '模型库',
|
||||
// icon: 'apps-fill',
|
||||
// },
|
||||
// {
|
||||
// key: '9',
|
||||
// title: '文档',
|
||||
// icon: 'book-fill',
|
||||
// },
|
||||
//
|
||||
// {
|
||||
// key: '6',
|
||||
// title: '新手引导',
|
||||
// icon: 'dashboard-fill',
|
||||
// },
|
||||
// {
|
||||
// key: '3',
|
||||
// divider: true,
|
||||
// },
|
||||
{
|
||||
key: '4',
|
||||
title: '退出登录',
|
||||
@@ -130,7 +130,9 @@ function handleClick(item: any) {
|
||||
ElMessage.warning('暂未开放');
|
||||
break;
|
||||
case '5':
|
||||
openDialog();
|
||||
// 打开控制台
|
||||
popoverRef.value?.hide?.();
|
||||
router.push('/console');
|
||||
break;
|
||||
case '6':
|
||||
handleStartTutorial();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 检查是否在聊天页面
|
||||
const isOnChatPage = computed(() => {
|
||||
return route.path.startsWith('/chat');
|
||||
});
|
||||
|
||||
function goToChat() {
|
||||
router.push('/chat/conversation');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!isOnChatPage" class="start-chat-btn-container" data-tour="start-chat-btn">
|
||||
<div
|
||||
class="start-chat-btn"
|
||||
title="开始聊天"
|
||||
@click="goToChat"
|
||||
>
|
||||
<el-icon class="chat-icon">
|
||||
<i-ep-chat-dot-round />
|
||||
</el-icon>
|
||||
<span class="btn-text">开始聊天</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.start-chat-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
|
||||
.start-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.chat-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.start-chat-btn-container {
|
||||
margin-right: 8px;
|
||||
|
||||
.start-chat-btn {
|
||||
padding: 8px;
|
||||
|
||||
.btn-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core';
|
||||
|
||||
// 使用 VueUse 的 useColorMode
|
||||
const mode = useColorMode({
|
||||
attribute: 'class',
|
||||
modes: {
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
// 切换主题
|
||||
function toggleTheme() {
|
||||
mode.value = mode.value === 'dark' ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
// 主题图标
|
||||
const themeIcon = computed(() => {
|
||||
return mode.value === 'dark' ? 'Sunny' : 'Moon';
|
||||
});
|
||||
|
||||
// 主题标题
|
||||
const themeTitle = computed(() => {
|
||||
return mode.value === 'dark' ? '切换到浅色模式' : '切换到深色模式';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-btn-container" data-tour="theme-btn">
|
||||
<div
|
||||
class="theme-btn"
|
||||
:title="themeTitle"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<!-- PC端显示文字 + 图标 -->
|
||||
<el-icon class="theme-icon">
|
||||
<component :is="`i-ep-${themeIcon}`" />
|
||||
</el-icon>
|
||||
<span class="pc-text">主题</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.theme-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.theme-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
font-size: 18px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
&:hover .theme-icon {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
// PC端显示文字
|
||||
.pc-text {
|
||||
display: inline;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.theme-btn-container {
|
||||
.theme-btn {
|
||||
padding: 8px;
|
||||
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,94 +1,50 @@
|
||||
<!-- Header 头部 -->
|
||||
<!--
|
||||
<!– Header 头部 –>
|
||||
<script setup lang="ts">
|
||||
import { onKeyStroke } from '@vueuse/core';
|
||||
import { SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useDesignStore, useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
import { useRouter } from 'vue-router';
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import { useUserStore } from '@/stores';
|
||||
import AiTutorialBtn from './components/AiTutorialBtn.vue';
|
||||
import AnnouncementBtn from './components/AnnouncementBtn.vue';
|
||||
import Avatar from './components/Avatar.vue';
|
||||
import BuyBtn from './components/BuyBtn.vue';
|
||||
import Collapse from './components/Collapse.vue';
|
||||
import ConsoleBtn from './components/ConsoleBtn.vue';
|
||||
import CreateChat from './components/CreateChat.vue';
|
||||
import LoginBtn from './components/LoginBtn.vue';
|
||||
import ModelLibraryBtn from './components/ModelLibraryBtn.vue';
|
||||
import TitleEditing from './components/TitleEditing.vue';
|
||||
import StartChatBtn from './components/StartChatBtn.vue';
|
||||
import ThemeBtn from './components/ThemeBtn.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const avatarRef = ref();
|
||||
|
||||
const currentSession = computed(() => sessionStore.currentSession);
|
||||
|
||||
onMounted(() => {
|
||||
// 全局设置侧边栏默认宽度 (这个是不变的,一开始就设置)
|
||||
document.documentElement.style.setProperty(`--sidebar-default-width`, `${SIDE_BAR_WIDTH}px`);
|
||||
if (designStore.isCollapse) {
|
||||
document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
|
||||
}
|
||||
else {
|
||||
document.documentElement.style.setProperty(
|
||||
`--sidebar-left-container-default-width`,
|
||||
`${SIDE_BAR_WIDTH}px`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 定义 Ctrl+K 的处理函数
|
||||
function handleCtrlK(event: KeyboardEvent) {
|
||||
event.preventDefault(); // 防止默认行为
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 设置全局的键盘按键监听
|
||||
onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtrlK, {
|
||||
passive: false,
|
||||
});
|
||||
|
||||
// 打开控制台
|
||||
function handleOpenConsole() {
|
||||
avatarRef.value?.openDialog?.();
|
||||
router.push('/console');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="header-box relative z-10 top-0 left-0 right-0">
|
||||
<div class="absolute left-0 right-0 top-0 bottom-0 flex items-center flex-row">
|
||||
<div
|
||||
class="overflow-hidden flex h-full items-center flex-row flex-1 w-fit flex-shrink-0 min-w-0"
|
||||
>
|
||||
<div class="w-full flex items-center flex-row">
|
||||
<!-- 左边 -->
|
||||
<div
|
||||
v-if="designStore.isCollapse"
|
||||
class="left-box flex h-full items-center pl-20px gap-12px flex-shrink-0 flex-row"
|
||||
>
|
||||
<Collapse />
|
||||
<CreateChat />
|
||||
<div v-if="currentSession" class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
|
||||
</div>
|
||||
|
||||
<!-- 中间 -->
|
||||
<div class="middle-box flex-1 min-w-0 ml-12px">
|
||||
<TitleEditing />
|
||||
</div>
|
||||
<div class="header-box">
|
||||
<!– 左侧logo和品牌区域 –>
|
||||
<div class="left-section">
|
||||
<div class="brand-container">
|
||||
<el-image :src="logo" alt="logo" fit="contain" class="logo-img" />
|
||||
<span class="brand-text">意心AI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右边 -->
|
||||
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row">
|
||||
<!– 右侧功能按钮区域 –>
|
||||
<div class="right-section">
|
||||
<StartChatBtn />
|
||||
<AnnouncementBtn />
|
||||
<ModelLibraryBtn />
|
||||
<AiTutorialBtn />
|
||||
<ConsoleBtn @open-console="handleOpenConsole" />
|
||||
<BuyBtn v-show="userStore.userInfo" />
|
||||
<Avatar v-show="userStore.userInfo" ref="avatarRef" />
|
||||
<ThemeBtn />
|
||||
<LoginBtn v-show="!userStore.userInfo" />
|
||||
</div>
|
||||
<Avatar v-show="userStore.userInfo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,20 +54,483 @@ function handleOpenConsole() {
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
height: var(--header-container-default-height, 60px);
|
||||
|
||||
.header-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
width: calc(
|
||||
100% - var(--sidebar-left-container-default-width, 0px) - var(
|
||||
--sidebar-right-container-default-width,
|
||||
0px
|
||||
)
|
||||
);
|
||||
height: var(--header-container-default-heigth);
|
||||
margin: 0 var(--sidebar-right-container-default-width, 0) 0
|
||||
var(--sidebar-left-container-default-width, 0);
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
background: var(--header-bg-color, #ffffff);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
// 左侧品牌区域
|
||||
.left-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: fit-content;
|
||||
flex-shrink: 0;
|
||||
|
||||
.brand-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.logo-img {
|
||||
width: 36px; // 优化为更合适的大小
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 22px; // 减小字体大小
|
||||
font-weight: bold;
|
||||
color: var(--brand-color, #000000);
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.5px;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
//color: var(--brand-hover-color, #40a9ff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧功能区域
|
||||
.right-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px; // 优化按钮间距
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
|
||||
// 统一按钮样式
|
||||
:deep(.menu-button) {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-->
|
||||
<!-- Header 头部 -->
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import ConsoleBtn from '@/layouts/components0/Header/components/ConsoleBtn.vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
import AiTutorialBtn from './components/AiTutorialBtn.vue';
|
||||
import AnnouncementBtn from './components/AnnouncementBtn.vue';
|
||||
import Avatar from './components/Avatar.vue';
|
||||
import BuyBtn from './components/BuyBtn.vue';
|
||||
import LoginBtn from './components/LoginBtn.vue';
|
||||
import ModelLibraryBtn from './components/ModelLibraryBtn.vue';
|
||||
import ThemeBtn from './components/ThemeBtn.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 当前激活的菜单项
|
||||
const activeIndex = computed(() => {
|
||||
if (route.path.startsWith('/console'))
|
||||
return 'console';
|
||||
if (route.path.startsWith('/model-library'))
|
||||
return 'model-library';
|
||||
if (route.path.includes('/chat/'))
|
||||
return 'chat';
|
||||
return '';
|
||||
});
|
||||
|
||||
// 导航处理
|
||||
function handleSelect(key: string) {
|
||||
if (key && key !== 'no-route') {
|
||||
router.push(key);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
class="header-menu"
|
||||
mode="horizontal"
|
||||
:ellipsis="false"
|
||||
:router="false"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<!-- 左侧品牌区域 -->
|
||||
<div class="menu-left">
|
||||
<div class="brand-container" @click="router.push('/')">
|
||||
<el-image :src="logo" alt="logo" fit="contain" class="logo-img" />
|
||||
<span class="brand-text">意心AI</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧功能区域 -->
|
||||
<div class="menu-right">
|
||||
<!-- AI聊天菜单 -->
|
||||
<el-sub-menu index="chat" class="chat-submenu" popper-class="custom-popover">
|
||||
<template #title>
|
||||
<span class="menu-title">AI聊天</span>
|
||||
</template>
|
||||
<el-menu-item index="/chat/conversation">
|
||||
AI对话
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/chat/image">
|
||||
图片生成
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/chat/video">
|
||||
视频生成
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 公告按钮 -->
|
||||
<el-menu-item class="custom-menu-item" index="no-route">
|
||||
<AnnouncementBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 模型库 -->
|
||||
<el-menu-item index="/model-library" class="custom-menu-item">
|
||||
<ModelLibraryBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- AI教程 -->
|
||||
<el-menu-item class="custom-menu-item" index="no-route">
|
||||
<AiTutorialBtn />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 控制台菜单 -->
|
||||
<el-sub-menu index="console" class="console-submenu" popper-class="custom-popover">
|
||||
<template #title>
|
||||
<ConsoleBtn />
|
||||
</template>
|
||||
<el-menu-item index="/console/user">
|
||||
用户信息
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/apikey">
|
||||
API密钥
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/recharge-log">
|
||||
充值记录
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/usage">
|
||||
用量统计
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/premium">
|
||||
尊享服务
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/daily-task">
|
||||
每日任务
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/invite">
|
||||
每周邀请
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/console/activation">
|
||||
激活码兑换
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
|
||||
<!-- 购买按钮 -->
|
||||
<el-menu-item v-if="userStore.userInfo" class="custom-menu-item" index="no-route">
|
||||
<BuyBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 主题切换(暂不显示) -->
|
||||
<el-menu-item v-if="false" class="custom-menu-item" index="no-route">
|
||||
<ThemeBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 用户头像 -->
|
||||
<div v-if="userStore.userInfo" class="avatar-container">
|
||||
<Avatar />
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<el-menu-item v-if="!userStore.userInfo" class="login-menu-item" index="no-route">
|
||||
<LoginBtn :is-menu-item="true" />
|
||||
</el-menu-item>
|
||||
</div>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header-container {
|
||||
--menu-hover-bg: #f5f5f5;
|
||||
--menu-active-color: var(--el-color-primary);
|
||||
--menu-transition: all 0.2s ease;
|
||||
|
||||
width: 100%;
|
||||
height: var(--header-container-default-height, 64px);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
user-select: none;
|
||||
|
||||
}
|
||||
|
||||
.header-menu {
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
// 左侧品牌区域
|
||||
.menu-left {
|
||||
flex-shrink: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.brand-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: background-color var(--menu-transition);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--menu-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
transition: transform var(--menu-transition);
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--brand-color, #000000);
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
// 右侧功能区域
|
||||
.menu-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-right: 16px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
// 公共菜单项样式
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
height: 100% !important;
|
||||
border-bottom: none !important;
|
||||
padding: 0 12px !important;
|
||||
color: inherit !important;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
color: var(--menu-active-color) !important;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: transparent !important;
|
||||
color: var(--menu-active-color) !important;
|
||||
|
||||
.menu-title {
|
||||
color: var(--menu-active-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天和控制台子菜单
|
||||
.chat-submenu,
|
||||
.console-submenu {
|
||||
:deep(.el-sub-menu__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
// 自定义按钮菜单项
|
||||
.custom-menu-item,
|
||||
.login-menu-item {
|
||||
:deep(.el-menu-item-content) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar 容器
|
||||
.avatar-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 1280px) {
|
||||
.brand-text {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.menu-right {
|
||||
margin-right: 12px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.brand-container {
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.brand-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.menu-left {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.menu-right {
|
||||
margin-right: 8px;
|
||||
|
||||
// 隐藏按钮文字
|
||||
:deep(.button-text) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 显示图标
|
||||
:deep(.el-icon) {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
padding: 0 8px !important;
|
||||
min-width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.menu-right {
|
||||
gap: 0;
|
||||
|
||||
:deep(.el-menu-item),
|
||||
:deep(.el-sub-menu__title) {
|
||||
padding: 0 6px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// 自定义弹出框样式
|
||||
.custom-popover {
|
||||
.el-menu {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 0;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.el-menu-item {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0 20px;
|
||||
margin: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
725
Yi.Ai.Vue3/src/layouts/components0/Aside/index.vue
Normal file
725
Yi.Ai.Vue3/src/layouts/components0/Aside/index.vue
Normal file
@@ -0,0 +1,725 @@
|
||||
<script setup lang="ts">
|
||||
import type { ConversationItem } from 'vue-element-plus-x/types/Conversations';
|
||||
import type { ChatSessionVo } from '@/api/session/types';
|
||||
import { ChatLineSquare, Expand, Fold, MoreFilled, Plus } from '@element-plus/icons-vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { get_session } from '@/api';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
const conversationsList = computed(() => sessionStore.sessionList);
|
||||
const loadMoreLoading = computed(() => sessionStore.isLoadingMore);
|
||||
const active = ref<string | undefined>();
|
||||
const isCollapsed = computed(() => designStore.isCollapseConversationList);
|
||||
|
||||
onMounted(async () => {
|
||||
await sessionStore.requestSessionList();
|
||||
if (conversationsList.value.length > 0 && sessionId.value) {
|
||||
const currentSessionRes = await get_session(`${sessionId.value}`);
|
||||
sessionStore.setCurrentSession(currentSessionRes.data);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => sessionStore.currentSession,
|
||||
(newValue) => {
|
||||
active.value = newValue ? `${newValue.id}` : undefined;
|
||||
},
|
||||
);
|
||||
|
||||
// 创建会话
|
||||
function handleCreatChat() {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function handleChange(item: ConversationItem<ChatSessionVo>) {
|
||||
sessionStore.setCurrentSession(item);
|
||||
router.replace({
|
||||
name: 'chatConversationWithId',
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 处理组件触发的加载更多事件
|
||||
async function handleLoadMore() {
|
||||
if (!sessionStore.hasMore)
|
||||
return;
|
||||
await sessionStore.loadMoreSessions();
|
||||
}
|
||||
|
||||
// 右键菜单
|
||||
function handleMenuCommand(command: string, item: ConversationItem<ChatSessionVo>) {
|
||||
switch (command) {
|
||||
case 'delete':
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
break;
|
||||
case 'rename':
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: item.sessionTitle,
|
||||
autofocus: false,
|
||||
inputValidator: (value) => {
|
||||
return !!value;
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: item.id!,
|
||||
sessionTitle: value,
|
||||
sessionContent: item.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
if (sessionStore.currentSession?.id === item.id) {
|
||||
sessionStore.setCurrentSession({
|
||||
...item,
|
||||
sessionTitle: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠/展开侧边栏
|
||||
function toggleSidebar() {
|
||||
designStore.setIsCollapseConversationList(!designStore.isCollapseConversationList);
|
||||
}
|
||||
|
||||
// 点击logo创建新会话(仅在折叠状态)
|
||||
function handleLogoClick() {
|
||||
if (isCollapsed.value) {
|
||||
handleCreatChat();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理右键菜单
|
||||
function handleContextMenu(event: MouseEvent, item: ConversationItem<ChatSessionVo>) {
|
||||
event.preventDefault();
|
||||
// 在折叠状态下触发菜单
|
||||
ElMessageBox.confirm('删除后,聊天记录将不可恢复。', '确定删除对话?', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(() => {
|
||||
sessionStore.deleteSessions([item.id!]);
|
||||
nextTick(() => {
|
||||
if (item.id === active.value) {
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="aside-container"
|
||||
:class="{ 'aside-collapsed': isCollapsed }"
|
||||
>
|
||||
<div class="aside-wrapper">
|
||||
<!-- 头部 -->
|
||||
<div class="aside-header">
|
||||
<!-- 展开状态显示logo和标题 -->
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
class="header-content-expanded flex items-center gap-8px hover:cursor-pointer"
|
||||
@click="handleCreatChat"
|
||||
>
|
||||
<span class="logo-text max-w-150px text-overflow">会话</span>
|
||||
</div>
|
||||
|
||||
<!-- 折叠状态只显示logo -->
|
||||
<div
|
||||
v-else
|
||||
class="header-content-collapsed flex items-center justify-center hover:cursor-pointer"
|
||||
@click="handleLogoClick"
|
||||
>
|
||||
<el-icon size="20">
|
||||
<ChatLineSquare />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 折叠按钮 -->
|
||||
<el-tooltip
|
||||
:content="isCollapsed ? '展开侧边栏' : '折叠侧边栏'"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-button
|
||||
class="collapse-btn"
|
||||
type="text"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<el-icon v-if="isCollapsed">
|
||||
<Expand />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Fold />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="aside-body">
|
||||
<!-- 创建会话按钮 -->
|
||||
<div class="creat-chat-btn-wrapper">
|
||||
<div
|
||||
class="creat-chat-btn"
|
||||
:class="{ 'creat-chat-btn-collapsed': isCollapsed }"
|
||||
@click="handleCreatChat"
|
||||
>
|
||||
<el-icon class="add-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span v-if="!isCollapsed" class="creat-chat-text">
|
||||
新对话
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<div class="aside-content">
|
||||
<div v-if="conversationsList.length > 0" class="conversations-wrap">
|
||||
<Conversations
|
||||
v-model:active="active"
|
||||
:items="conversationsList"
|
||||
:label-max-width="200"
|
||||
:show-tooltip="!isCollapsed"
|
||||
:tooltip-offset="60"
|
||||
show-built-in-menu
|
||||
groupable
|
||||
row-key="id"
|
||||
label-key="sessionTitle"
|
||||
:tooltip-placement="isCollapsed ? 'right-start' : 'right'"
|
||||
:load-more="handleLoadMore"
|
||||
:load-more-loading="loadMoreLoading"
|
||||
:items-style="{
|
||||
marginLeft: '8px',
|
||||
marginRight: '8px',
|
||||
userSelect: 'none',
|
||||
borderRadius: isCollapsed ? '12px' : '10px',
|
||||
padding: isCollapsed ? '12px 8px' : '8px 12px',
|
||||
justifyContent: isCollapsed ? 'center' : 'space-between',
|
||||
width: isCollapsed ? '64px' : 'auto',
|
||||
height: isCollapsed ? '64px' : 'auto',
|
||||
minHeight: '48px',
|
||||
flexDirection: isCollapsed ? 'column' : 'row',
|
||||
position: 'relative',
|
||||
}"
|
||||
:items-active-style="{
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)',
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
}"
|
||||
:items-hover-style="{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
}"
|
||||
@menu-command="handleMenuCommand"
|
||||
@change="handleChange"
|
||||
@contextmenu="handleContextMenu"
|
||||
>
|
||||
<!-- 自定义折叠状态下的会话项内容 -->
|
||||
<template #default="{ item }">
|
||||
<div class="conversation-item-content">
|
||||
<div v-if="isCollapsed" class="collapsed-item">
|
||||
<div
|
||||
class="avatar-circle"
|
||||
@contextmenu="(e) => handleContextMenu(e, item)"
|
||||
>
|
||||
{{ item.sessionTitle?.charAt(0) || 'A' }}
|
||||
</div>
|
||||
<div v-if="item.unreadCount" class="unread-indicator">
|
||||
{{ item.unreadCount }}
|
||||
</div>
|
||||
<!-- 折叠状态下的更多操作按钮 -->
|
||||
<div
|
||||
class="collapsed-menu-trigger"
|
||||
@click.stop="handleMenuCommand('rename', item)"
|
||||
@contextmenu.stop="(e) => handleContextMenu(e, item)"
|
||||
>
|
||||
<el-icon size="14">
|
||||
<MoreFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="expanded-item">
|
||||
<div class="conversation-info">
|
||||
<div class="conversation-title">
|
||||
{{ item.sessionTitle }}
|
||||
</div>
|
||||
<div v-if="item.sessionContent" class="conversation-preview">
|
||||
{{ item.sessionContent.substring(0, 30) }}{{ item.sessionContent.length > 30 ? '...' : '' }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 展开状态下的更多操作按钮(Conversations组件自带) -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Conversations>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-else
|
||||
class="h-full flex-center"
|
||||
:description="isCollapsed ? '' : '暂无对话记录'"
|
||||
>
|
||||
<template #description>
|
||||
<span v-if="!isCollapsed">暂无对话记录</span>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 基础样式
|
||||
.aside-container {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
border-right: 0.5px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--sidebar-background-color, #f9fafb);
|
||||
|
||||
&.aside-collapsed {
|
||||
width: 100px;
|
||||
|
||||
.aside-wrapper {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.conversations-wrap {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 240px;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
// 头部样式
|
||||
.aside-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 56px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--s-color-border-tertiary, rgb(0 0 0 / 8%));
|
||||
background-color: var(--sidebar-header-bg, #ffffff);
|
||||
|
||||
.header-content-expanded {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-content-collapsed {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
transform: skewX(-2deg);
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-text-color-primary);
|
||||
background-color: var(--el-fill-color-light);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.aside-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.creat-chat-btn-wrapper {
|
||||
padding: 12px 8px 4px;
|
||||
|
||||
.creat-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
color: #0057ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgb(0 87 255 / 6%);
|
||||
border: 1px solid rgb(0 102 255 / 15%);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(0 87 255 / 12%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.creat-chat-btn-collapsed {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.creat-chat-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
margin-left: 6px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.conversations-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0 4px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.conversation-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.collapsed-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.avatar-circle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background-color: #ff4d4f;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.collapsed-menu-trigger {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 2;
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .collapsed-menu-trigger {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.conversation-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
|
||||
.conversation-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.conversation-preview {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 样式穿透 - 重点优化操作按钮区域
|
||||
:deep() {
|
||||
.conversations-list {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.conversation-group-title {
|
||||
padding-left: 12px !important;
|
||||
background-color: transparent !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.title-text {
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 确保操作按钮区域在折叠状态下可见
|
||||
.conversation-item-actions {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.el-button {
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠状态样式
|
||||
.aside-collapsed {
|
||||
.conversation-group-title {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
justify-content: center !important;
|
||||
padding: 12px 8px !important;
|
||||
height: 64px !important;
|
||||
min-height: 64px !important;
|
||||
|
||||
&-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
// 隐藏默认的操作按钮,使用自定义的
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.aside-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
width: 280px !important;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
|
||||
&.aside-collapsed {
|
||||
transform: translateX(-100%);
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
&:not(.aside-collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.aside-wrapper {
|
||||
width: 280px !important;
|
||||
|
||||
.aside-collapsed & {
|
||||
width: 100px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端遮罩层
|
||||
.aside-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
<!-- DesignConfig -->
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>配置页面</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
// 打开AI使用教程(跳转到外部链接)
|
||||
function openTutorial() {
|
||||
window.open('https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde', '_blank');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ai-tutorial-btn-container" data-tour="ai-tutorial-link">
|
||||
<div
|
||||
class="ai-tutorial-btn"
|
||||
title="点击跳转YiXinAI玩法指南专栏"
|
||||
@click="openTutorial"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">文档</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon w-6 h-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 14l9-5-9-5-9 5 9 5z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 14l6.16-3.422A12.083 12.083 0 0118 13.5c0 2.579-3.582 4.5-6 4.5s-6-1.921-6-4.5c0-.432.075-.85.198-1.244L12 14z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-tutorial-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ai-tutorial-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #E6A23C;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #F1B44C;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.ai-tutorial-btn-container {
|
||||
.ai-tutorial-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useAnnouncementStore } from '@/stores';
|
||||
|
||||
const announcementStore = useAnnouncementStore();
|
||||
const { announcements } = storeToRefs(announcementStore);
|
||||
|
||||
// 计算未读公告数量(系统公告数量)
|
||||
const unreadCount = computed(() => {
|
||||
if (!Array.isArray(announcements.value))
|
||||
return 0;
|
||||
return announcements.value.filter(a => a.type === 'System').length;
|
||||
});
|
||||
|
||||
// 打开公告弹窗
|
||||
function openAnnouncement() {
|
||||
announcementStore.openDialog();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="announcement-btn-container" data-tour="announcement-btn">
|
||||
<el-badge
|
||||
is-dot
|
||||
class="announcement-badge"
|
||||
>
|
||||
<!-- :value="unreadCount" -->
|
||||
<!-- :hidden="unreadCount === 0" -->
|
||||
<!-- :max="99" -->
|
||||
<div
|
||||
class="announcement-btn"
|
||||
title="查看公告"
|
||||
@click="openAnnouncement"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">公告</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
</div>
|
||||
</el-badge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.announcement-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.announcement-badge {
|
||||
:deep(.el-badge__content) {
|
||||
background-color: #f56c6c;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.announcement-btn-container {
|
||||
.announcement-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
499
Yi.Ai.Vue3/src/layouts/components0/Header/components/Avatar.vue
Normal file
499
Yi.Ai.Vue3/src/layouts/components0/Header/components/Avatar.vue
Normal file
@@ -0,0 +1,499 @@
|
||||
<!-- 头像 -->
|
||||
<script setup lang="ts">
|
||||
import { ChatLineRound } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Popover from '@/components/Popover/index.vue';
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||
import { useAnnouncementStore, useGuideTourStore, useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
import { getUserProfilePicture, isUserVip } from '@/utils/user';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const sessionStore = useSessionStore();
|
||||
const guideTourStore = useGuideTourStore();
|
||||
const announcementStore = useAnnouncementStore();
|
||||
const { startUserCenterTour } = useGuideTour();
|
||||
|
||||
/* 弹出面板 开始 */
|
||||
const popoverStyle = ref({
|
||||
width: '200px',
|
||||
padding: '4px',
|
||||
height: 'fit-content',
|
||||
});
|
||||
const popoverRef = ref();
|
||||
|
||||
// 弹出面板内容
|
||||
const popoverList = ref([
|
||||
|
||||
{
|
||||
key: '5',
|
||||
title: '控制台',
|
||||
icon: 'settings-4-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
key: '7',
|
||||
title: '公告',
|
||||
icon: 'notification-fill',
|
||||
},
|
||||
{
|
||||
key: '8',
|
||||
title: '模型库',
|
||||
icon: 'apps-fill',
|
||||
},
|
||||
{
|
||||
key: '9',
|
||||
title: '文档',
|
||||
icon: 'book-fill',
|
||||
},
|
||||
|
||||
{
|
||||
key: '6',
|
||||
title: '新手引导',
|
||||
icon: 'dashboard-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
title: '退出登录',
|
||||
icon: 'logout-box-r-line',
|
||||
},
|
||||
]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const rechargeLogRef = ref();
|
||||
const activeNav = ref('user');
|
||||
|
||||
// ============ 邀请码分享功能 ============
|
||||
/** 从 URL 获取的邀请码 */
|
||||
const externalInviteCode = ref<string>('');
|
||||
|
||||
const navItems = [
|
||||
{ name: 'user', label: '用户信息', icon: 'User' },
|
||||
// { name: 'role', label: '角色管理', icon: 'Avatar' },
|
||||
// { name: 'permission', label: '权限管理', icon: 'Key' },
|
||||
// { name: 'userInfo', label: '用户信息', icon: 'User' },
|
||||
{ name: 'apiKey', label: 'API密钥', icon: 'Key' },
|
||||
|
||||
{ name: 'rechargeLog', label: '充值记录', icon: 'Document' },
|
||||
{ name: 'usageStatistics', label: '用量统计', icon: 'Histogram' },
|
||||
{ name: 'premiumService', label: '尊享服务', icon: 'ColdDrink' },
|
||||
{ name: 'dailyTask', label: '每日任务(限时)', icon: 'Trophy' },
|
||||
{ name: 'cardFlip', label: '每周邀请(限时)', icon: 'Present' },
|
||||
// { name: 'usageStatistics2', label: '用量统计2', icon: 'Histogram' },
|
||||
{ name: 'activationCode', label: '激活码兑换', icon: 'MagicStick' },
|
||||
];
|
||||
function openDialog() {
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
function handleConfirm(activeNav: string) {
|
||||
ElMessage.success('操作成功');
|
||||
}
|
||||
|
||||
// 导航切换
|
||||
function handleNavChange(nav: string) {
|
||||
activeNav.value = nav;
|
||||
// 同步更新 store 中的 tab 状态,防止下次通过 store 打开同一 tab 时因值未变而不触发 watch
|
||||
if (userStore.userCenterActiveTab !== nav) {
|
||||
userStore.userCenterActiveTab = nav;
|
||||
}
|
||||
}
|
||||
|
||||
// 联系售后
|
||||
function handleContactSupport() {
|
||||
rechargeLogRef.value?.contactCustomerService();
|
||||
}
|
||||
const { startHeaderTour } = useGuideTour();
|
||||
|
||||
// 开始引导教程
|
||||
function handleStartTutorial() {
|
||||
startHeaderTour();
|
||||
}
|
||||
// 点击
|
||||
function handleClick(item: any) {
|
||||
switch (item.key) {
|
||||
case '1':
|
||||
ElMessage.warning('暂未开放');
|
||||
break;
|
||||
case '2':
|
||||
ElMessage.warning('暂未开放');
|
||||
break;
|
||||
case '5':
|
||||
// 打开控制台
|
||||
popoverRef.value?.hide?.();
|
||||
router.push('/console');
|
||||
break;
|
||||
case '6':
|
||||
handleStartTutorial();
|
||||
break;
|
||||
case '7':
|
||||
// 打开公告
|
||||
popoverRef.value?.hide?.();
|
||||
announcementStore.openDialog();
|
||||
break;
|
||||
case '8':
|
||||
// 打开模型库
|
||||
popoverRef.value?.hide?.();
|
||||
router.push('/model-library');
|
||||
break;
|
||||
case '9':
|
||||
// 打开文档
|
||||
popoverRef.value?.hide?.();
|
||||
window.open('https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde', '_blank');
|
||||
break;
|
||||
case '4':
|
||||
popoverRef.value?.hide?.();
|
||||
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
|
||||
confirmButtonText: '确认退出',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
autofocus: false,
|
||||
})
|
||||
.then(async () => {
|
||||
// 在这里执行退出方法
|
||||
await userStore.logout();
|
||||
// 清空回话列表并回到默认页
|
||||
await sessionStore.requestSessionList(1, true);
|
||||
await sessionStore.createSessionBtn();
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '退出成功',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// ElMessage({
|
||||
// type: 'info',
|
||||
// message: '取消',
|
||||
// });
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function openVipGuide() {
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
<div class="text-center leading-relaxed">
|
||||
<h3 class="text-lg font-bold mb-3">${isUserVip() ? 'YiXinAI-VIP 会员' : '成为 YiXinAI-VIP'}</h3>
|
||||
<p class="mb-2">
|
||||
${
|
||||
isUserVip()
|
||||
? '您已是尊贵会员,享受全部 AI 模型与专属服务。感谢支持!'
|
||||
: '解锁所有 AI 模型,无限加速,专属客服,尽享尊贵体验。'
|
||||
}
|
||||
</p>
|
||||
${
|
||||
isUserVip()
|
||||
? '<p class="text-sm text-gray-500">您可随时访问产品页面查看更多特权内容。</p>'
|
||||
: '<p class="text-sm text-gray-500">点击下方按钮,立即升级为 VIP 会员!</p>'
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
isUserVip() ? '会员状态' : '会员尊享',
|
||||
{
|
||||
confirmButtonText: '前往产品页面',
|
||||
cancelButtonText: '关闭',
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: 'info',
|
||||
center: true,
|
||||
roundButton: true,
|
||||
},
|
||||
)
|
||||
.then(() => {
|
||||
router.push({
|
||||
name: 'products', // 使用命名路由
|
||||
query: { from: isUserVip() ? 'vip' : 'user' }, // 可选:添加来源标识
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 点击右上角关闭或“关闭”按钮,不执行任何操作
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 监听对话框打开事件,切换到邀请码标签页 ============
|
||||
watch(dialogVisible, (newVal) => {
|
||||
if (newVal && externalInviteCode.value) {
|
||||
// 对话框打开后,切换标签页(已通过 :default-active 绑定,会自动响应)
|
||||
// console.log('[Avatar] watch: 对话框已打开,切换到 cardFlip 标签页');
|
||||
nextTick(() => {
|
||||
activeNav.value = 'cardFlip';
|
||||
// console.log('[Avatar] watch: 已设置 activeNav 为', activeNav.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 对话框关闭时,清除邀请码状态和 URL 参数
|
||||
if (!newVal && externalInviteCode.value) {
|
||||
// console.log('[Avatar] watch: 对话框关闭,清除邀请码状态');
|
||||
externalInviteCode.value = '';
|
||||
|
||||
// 清除 URL 中的 inviteCode 参数
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.has('inviteCode')) {
|
||||
url.searchParams.delete('inviteCode');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
// console.log('[Avatar] watch: 已清除 URL 中的 inviteCode 参数');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听 URL 参数,实现邀请码快捷分享 ============
|
||||
onMounted(() => {
|
||||
// 获取 URL 查询参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const inviteCode = urlParams.get('inviteCode');
|
||||
|
||||
if (inviteCode && inviteCode.trim()) {
|
||||
// console.log('[Avatar] onMounted: 检测到邀请码', inviteCode);
|
||||
|
||||
// 保存邀请码
|
||||
externalInviteCode.value = inviteCode.trim();
|
||||
|
||||
// 先设置标签页为 cardFlip
|
||||
activeNav.value = 'cardFlip';
|
||||
// console.log('[Avatar] onMounted: 设置 activeNav 为', activeNav.value);
|
||||
|
||||
// 延迟打开对话框,确保状态已更新
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
// console.log('[Avatar] onMounted: 打开用户中心对话框');
|
||||
dialogVisible.value = true;
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// 注意:不立即清除 URL 参数,保留给登录后使用
|
||||
// URL 参数会在对话框关闭时清除
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听引导状态,自动打开用户中心并开始引导 ============
|
||||
watch(() => guideTourStore.shouldStartUserCenterTour, (shouldStart) => {
|
||||
if (shouldStart) {
|
||||
// 清除触发标记
|
||||
guideTourStore.clearUserCenterTourTrigger();
|
||||
|
||||
// 注册导航切换回调
|
||||
guideTourStore.setUserCenterNavChangeCallback((nav: string) => {
|
||||
activeNav.value = nav;
|
||||
});
|
||||
|
||||
// 注册关闭弹窗回调
|
||||
guideTourStore.setUserCenterCloseCallback(() => {
|
||||
dialogVisible.value = false;
|
||||
});
|
||||
|
||||
// 打开用户中心弹窗
|
||||
nextTick(() => {
|
||||
dialogVisible.value = true;
|
||||
|
||||
// 等待弹窗打开后开始引导
|
||||
setTimeout(() => {
|
||||
startUserCenterTour();
|
||||
}, 600);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 监听 Store 状态,控制用户中心弹窗 (新增) ============
|
||||
watch(() => userStore.isUserCenterVisible, (val) => {
|
||||
dialogVisible.value = val;
|
||||
if (val && userStore.userCenterActiveTab) {
|
||||
activeNav.value = userStore.userCenterActiveTab;
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => userStore.userCenterActiveTab, (val) => {
|
||||
if (val) {
|
||||
activeNav.value = val;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听本地 dialogVisible 变化,同步回 Store(可选,为了保持一致性)
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
userStore.closeUserCenter();
|
||||
}
|
||||
});
|
||||
|
||||
// ============ 暴露方法供外部调用 ============
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2 ">
|
||||
<!-- 用户信息区域 -->
|
||||
<div class="user-info-display cursor-pointer flex flex-col text-right mr-2 leading-tight" @click="openDialog">
|
||||
<div class="text-sm font-semibold text-gray-800">
|
||||
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
||||
</div>
|
||||
|
||||
<!-- 角色展示 -->
|
||||
<div>
|
||||
<span
|
||||
v-if="isUserVip()"
|
||||
class="inline-block px-2 py-0.5 text-xs text-yellow-700 bg-yellow-100 rounded-full font-semibold"
|
||||
>
|
||||
YiXinAI-VIP
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="inline-block px-2 py-0.5 text-xs text-gray-600 bg-gray-100 rounded-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 transition"
|
||||
>
|
||||
普通用户
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 头像区域 -->
|
||||
<div class="avatar-container" data-tour="user-avatar">
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
placement="bottom-end"
|
||||
trigger="clickTarget"
|
||||
:trigger-style="{ cursor: 'pointer' }"
|
||||
popover-class="popover-content"
|
||||
:popover-style="popoverStyle"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-avatar :src="getUserProfilePicture()" :size="28" fit="fit" shape="circle" />
|
||||
</template>
|
||||
|
||||
<div class="popover-content-box shadow-lg">
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-info-box flex items-center gap-8px p-8px rounded-lg mb-2">
|
||||
<el-avatar :src="getUserProfilePicture()" :size="32" fit="fit" shape="circle" />
|
||||
<div class="flex flex-col text-sm">
|
||||
<div class="font-semibold text-gray-800">
|
||||
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<span
|
||||
v-if="isUserVip()"
|
||||
class="inline-block px-2 py-0.5 text-xs text-yellow-700 bg-yellow-100 rounded-full font-semibold"
|
||||
>
|
||||
YiXinAI-VIP
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="inline-block px-2 py-0.5 text-xs text-gray-600 bg-gray-100 rounded-full cursor-pointer hover:bg-yellow-50 hover:text-yellow-700 transition"
|
||||
>
|
||||
普通用户
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divder h-1px bg-gray-200 my-4px" />
|
||||
|
||||
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
|
||||
<div
|
||||
v-if="!item.divider"
|
||||
class="popover-content-box-item flex items-center h-full gap-8px p-8px pl-10px pr-12px rounded-lg hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<SvgIcon :name="item.icon!" size="16" class-name="flex-none" />
|
||||
<div class="popover-content-box-item-text font-size-14px text-overflow max-h-120px">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.divider" class="divder h-1px bg-gray-200 my-4px" />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
<nav-dialog
|
||||
v-model="dialogVisible"
|
||||
title="控制台"
|
||||
:nav-items="navItems"
|
||||
:default-active="activeNav"
|
||||
@confirm="handleConfirm"
|
||||
@nav-change="handleNavChange"
|
||||
>
|
||||
<template #extra-actions>
|
||||
<el-tooltip v-if="isUserVip() && activeNav === 'rechargeLog'" content="联系售后" placement="bottom">
|
||||
<el-button circle plain size="small" @click="handleContactSupport">
|
||||
<el-icon color="#07c160">
|
||||
<ChatLineRound />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<!-- 用户管理内容 -->
|
||||
<template #user>
|
||||
<user-management />
|
||||
</template>
|
||||
<!-- 用量统计 -->
|
||||
<template #usageStatistics>
|
||||
<usage-statistics />
|
||||
</template>
|
||||
<!-- 尊享服务 -->
|
||||
<template #premiumService>
|
||||
<premium-service />
|
||||
</template>
|
||||
<!-- 用量统计 -->
|
||||
<!-- <template #usageStatistics2> -->
|
||||
<!-- <usage-statistics2 /> -->
|
||||
<!-- </template> -->
|
||||
|
||||
<!-- 角色管理内容 -->
|
||||
<template #role>
|
||||
<!-- < /> -->
|
||||
</template>
|
||||
|
||||
<!-- 权限管理内容 -->
|
||||
<template #permission>
|
||||
<!-- <permission-management /> -->
|
||||
</template>
|
||||
|
||||
<template #apiKey>
|
||||
<APIKeyManagement />
|
||||
</template>
|
||||
<template #activationCode>
|
||||
<activation-code />
|
||||
</template>
|
||||
<template #dailyTask>
|
||||
<daily-task />
|
||||
</template>
|
||||
<template #cardFlip>
|
||||
<card-flip-activity :external-invite-code="externalInviteCode" />
|
||||
</template>
|
||||
<template #rechargeLog>
|
||||
<recharge-log ref="rechargeLogRef" />
|
||||
</template>
|
||||
</nav-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popover-content {
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
}
|
||||
.popover-content-box {
|
||||
padding: 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { showProductPackage } from '@/utils/product-package';
|
||||
|
||||
// 点击购买按钮
|
||||
function onProductPackage() {
|
||||
showProductPackage();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="buy-btn-container">
|
||||
<el-button
|
||||
class="buy-btn flex items-center gap-2 px-5 py-2 font-semibold shadow-lg"
|
||||
data-tour="buy-btn"
|
||||
@click="onProductPackage"
|
||||
>
|
||||
<span>立即购买</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.buy-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 22px 0 0;
|
||||
|
||||
.buy-btn {
|
||||
background: linear-gradient(90deg, #FFD700, #FFC107);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(255, 215, 0, 0.5);
|
||||
background: linear-gradient(90deg, #FFC107, #FFD700);
|
||||
}
|
||||
|
||||
.icon-rocket {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.animate-bounce {
|
||||
animation: bounce 1.2s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端,屏幕小于756px
|
||||
@media screen and (max-width: 756px) {
|
||||
.buy-btn-container {
|
||||
margin: 0 ;
|
||||
|
||||
.buy-btn {
|
||||
font-size: 12px;
|
||||
max-width: 60px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- 侧边栏折叠按钮 -->
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useCollapseToggle } from '@/hooks/useCollapseToggle';
|
||||
import { useDesignStore } from '@/stores';
|
||||
|
||||
const { changeCollapse } = useCollapseToggle();
|
||||
const designStore = useDesignStore();
|
||||
|
||||
function handleChangeCollapse() {
|
||||
changeCollapse();
|
||||
// 每次切换折叠状态,重置安全区状态
|
||||
designStore.isSafeAreaHover = false;
|
||||
// 重置首次激活悬停状态
|
||||
designStore.hasActivatedHover = false;
|
||||
if (!designStore.isCollapse) {
|
||||
document.documentElement.style.setProperty(
|
||||
`--sidebar-left-container-default-width`,
|
||||
`${SIDE_BAR_WIDTH}px`,
|
||||
);
|
||||
}
|
||||
else {
|
||||
document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="collapse-container btn-icon-btn" @click="handleChangeCollapse">
|
||||
<SvgIcon v-if="!designStore.isCollapse" name="ms-left-panel-close-outline" size="24" />
|
||||
<SvgIcon v-if="designStore.isCollapse" name="ms-left-panel-open-outline" size="24" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// .collapse-container {
|
||||
// }
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 打开用户中心对话框(通过调用 Avatar 组件的方法)
|
||||
function openConsole() {
|
||||
// 触发事件,由父组件处理
|
||||
emit('open-console');
|
||||
}
|
||||
|
||||
const emit = defineEmits(['open-console']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="console-btn-container" data-tour="console-btn">
|
||||
<div
|
||||
class="console-btn"
|
||||
title="打开控制台"
|
||||
@click="openConsole"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">控制台</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.console-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.console-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #909399;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.console-btn-container {
|
||||
.console-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- 添加新会话按钮 -->
|
||||
<script setup lang="ts">
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
/* 创建会话 开始 */
|
||||
function handleCreatChat() {
|
||||
if (!sessionStore.currentSession)
|
||||
return;
|
||||
// 创建会话, 跳转到默认聊天
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
/* 创建会话 结束 */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="create-chat-container flex-center flex-none p-6px pl-8px pr-8px c-#0057ff b-#0057ff b-rounded-12px border-1px hover:bg-#0057ff hover:c-#fff hover:b-#fff hover:cursor-pointer border-solid select-none"
|
||||
:class="{
|
||||
'is-disabled': !sessionStore.currentSession,
|
||||
}"
|
||||
@click="handleCreatChat"
|
||||
>
|
||||
<el-icon size="12" class="flex-center flex-none w-14px h-14px">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span class="ml-4px font-size-14px font-700">新对话</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
color: #0057ff;
|
||||
cursor: not-allowed;
|
||||
background-color: transparent;
|
||||
border-color: #0057ff;
|
||||
border-style: solid;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<!-- LoginBtn 登录按钮 -->
|
||||
<script setup lang="ts">
|
||||
import LoginDialog from '@/components/LoginDialog/index.vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isLoginDialogVisible = computed(() => userStore.isLoginDialogVisible);
|
||||
|
||||
// 点击登录按钮时调用Store方法打开弹框
|
||||
function handleClickLogin() {
|
||||
userStore.openLoginDialog();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-btn-wrapper">
|
||||
<div
|
||||
class="login-btn bg-#191c1f c-#fff font-size-14px rounded-8px flex-center text-overflow p-10px pl-12px pr-12px min-w-49px h-16px cursor-pointer hover:bg-#232629 select-none"
|
||||
@click="handleClickLogin"
|
||||
>
|
||||
登录
|
||||
</div>
|
||||
|
||||
<!-- 登录弹框 -->
|
||||
<LoginDialog v-model:visible="isLoginDialogVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goToModelLibrary() {
|
||||
router.push('/model-library');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="model-library-btn-container" data-tour="model-library-btn">
|
||||
<div
|
||||
class="model-library-btn"
|
||||
title="查看模型库"
|
||||
@click="goToModelLibrary"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">模型库</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<svg
|
||||
class="mobile-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.model-library-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.model-library-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #606266;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #606266;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.model-library-btn-container {
|
||||
.model-library-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 检查是否在聊天页面
|
||||
const isOnChatPage = computed(() => {
|
||||
return route.path.startsWith('/chat');
|
||||
});
|
||||
|
||||
function goToChat() {
|
||||
router.push('/chat/conversation');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!isOnChatPage" class="start-chat-btn-container" data-tour="start-chat-btn">
|
||||
<div
|
||||
class="start-chat-btn"
|
||||
title="开始聊天"
|
||||
@click="goToChat"
|
||||
>
|
||||
<el-icon class="chat-icon">
|
||||
<i-ep-chat-dot-round />
|
||||
</el-icon>
|
||||
<span class="btn-text">开始聊天</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.start-chat-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
|
||||
.start-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.chat-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.start-chat-btn-container {
|
||||
margin-right: 8px;
|
||||
|
||||
.start-chat-btn {
|
||||
padding: 8px;
|
||||
|
||||
.btn-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core';
|
||||
|
||||
// 使用 VueUse 的 useColorMode
|
||||
const mode = useColorMode({
|
||||
attribute: 'class',
|
||||
modes: {
|
||||
light: 'light',
|
||||
dark: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
// 切换主题
|
||||
function toggleTheme() {
|
||||
mode.value = mode.value === 'dark' ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
// 主题图标
|
||||
const themeIcon = computed(() => {
|
||||
return mode.value === 'dark' ? 'Sunny' : 'Moon';
|
||||
});
|
||||
|
||||
// 主题标题
|
||||
const themeTitle = computed(() => {
|
||||
return mode.value === 'dark' ? '切换到浅色模式' : '切换到深色模式';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-btn-container" data-tour="theme-btn">
|
||||
<div
|
||||
class="theme-btn"
|
||||
:title="themeTitle"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<!-- PC端显示文字 + 图标 -->
|
||||
<el-icon class="theme-icon">
|
||||
<component :is="`i-ep-${themeIcon}`" />
|
||||
</el-icon>
|
||||
<span class="pc-text">主题</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.theme-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.theme-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
font-size: 18px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
&:hover .theme-icon {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
// PC端显示文字
|
||||
.pc-text {
|
||||
display: inline;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.theme-btn-container {
|
||||
.theme-btn {
|
||||
padding: 8px;
|
||||
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- 标题编辑 -->
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const currentSession = computed(() => sessionStore.currentSession);
|
||||
|
||||
function handleClickTitle() {
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
confirmButtonClass: 'el-button--primary',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
inputValue: currentSession.value?.sessionTitle,
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
})
|
||||
.then(({ value }) => {
|
||||
sessionStore
|
||||
.updateSession({
|
||||
id: currentSession.value!.id,
|
||||
sessionTitle: value,
|
||||
sessionContent: currentSession.value!.sessionContent,
|
||||
})
|
||||
.then(() => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '修改成功',
|
||||
});
|
||||
nextTick(() => {
|
||||
// 如果是当前会话,则更新当前选中会话信息
|
||||
sessionStore.setCurrentSession({
|
||||
...currentSession.value,
|
||||
sessionTitle: value,
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// ElMessage({
|
||||
// type: 'info',
|
||||
// message: '取消修改',
|
||||
// });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentSession" class="w-full h-full flex flex-col justify-center">
|
||||
<div class="box-border mr-20px">
|
||||
<div
|
||||
class="title-editing-container p-4px w-fit max-w-full flex items-center justify-start cursor-pointer select-none hover:bg-[rgba(0,0,0,.04)] cursor-pointer rounded-md font-size-14px"
|
||||
@click="handleClickTitle"
|
||||
>
|
||||
<div class="text-overflow select-none pr-8px">
|
||||
{{ currentSession.sessionTitle }}
|
||||
</div>
|
||||
<SvgIcon name="draft-line" size="14" class="flex-none c-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.title-editing-container {
|
||||
transition: all 0.3s ease;
|
||||
&:hover {
|
||||
.svg-icon {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.svg-icon {
|
||||
display: none;
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { QuestionFilled } from '@element-plus/icons-vue';
|
||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||
|
||||
const { startHeaderTour } = useGuideTour();
|
||||
|
||||
// 开始引导教程
|
||||
function handleStartTutorial() {
|
||||
startHeaderTour();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tutorial-btn-container" data-tour="tutorial-btn">
|
||||
<div
|
||||
class="tutorial-btn"
|
||||
@click="handleStartTutorial"
|
||||
>
|
||||
<!-- PC端显示文字 -->
|
||||
<span class="pc-text">新手引导</span>
|
||||
<!-- 移动端显示图标 -->
|
||||
<el-icon class="mobile-icon" :size="20">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tutorial-btn-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.tutorial-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
// PC端显示文字,隐藏图标
|
||||
.pc-text {
|
||||
display: inline;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端显示图标,隐藏文字
|
||||
@media (max-width: 768px) {
|
||||
.tutorial-btn-container {
|
||||
.tutorial-btn {
|
||||
.pc-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
122
Yi.Ai.Vue3/src/layouts/components0/Header/index.vue
Normal file
122
Yi.Ai.Vue3/src/layouts/components0/Header/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<!-- Header 头部 -->
|
||||
<script setup lang="ts">
|
||||
import { onKeyStroke } from '@vueuse/core';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useDesignStore, useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
import AiTutorialBtn from './components/AiTutorialBtn.vue';
|
||||
import AnnouncementBtn from './components/AnnouncementBtn.vue';
|
||||
import Avatar from './components/Avatar.vue';
|
||||
import BuyBtn from './components/BuyBtn.vue';
|
||||
import Collapse from './components/Collapse.vue';
|
||||
import ConsoleBtn from './components/ConsoleBtn.vue';
|
||||
import CreateChat from './components/CreateChat.vue';
|
||||
import LoginBtn from './components/LoginBtn.vue';
|
||||
import ModelLibraryBtn from './components/ModelLibraryBtn.vue';
|
||||
import StartChatBtn from './components/StartChatBtn.vue';
|
||||
import ThemeBtn from './components/ThemeBtn.vue';
|
||||
import TitleEditing from './components/TitleEditing.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const designStore = useDesignStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const avatarRef = ref();
|
||||
|
||||
const currentSession = computed(() => sessionStore.currentSession);
|
||||
|
||||
onMounted(() => {
|
||||
// 全局设置侧边栏默认宽度 (这个是不变的,一开始就设置)
|
||||
document.documentElement.style.setProperty(`--sidebar-default-width`, `${SIDE_BAR_WIDTH}px`);
|
||||
if (designStore.isCollapse) {
|
||||
document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``);
|
||||
}
|
||||
else {
|
||||
document.documentElement.style.setProperty(
|
||||
`--sidebar-left-container-default-width`,
|
||||
`${SIDE_BAR_WIDTH}px`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 定义 Ctrl+K 的处理函数
|
||||
function handleCtrlK(event: KeyboardEvent) {
|
||||
event.preventDefault(); // 防止默认行为
|
||||
sessionStore.createSessionBtn();
|
||||
}
|
||||
|
||||
// 设置全局的键盘按键监听
|
||||
onKeyStroke(event => event.ctrlKey && event.key.toLowerCase() === 'k', handleCtrlK, {
|
||||
passive: false,
|
||||
});
|
||||
|
||||
// 打开控制台
|
||||
function handleOpenConsole() {
|
||||
router.push('/console');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="header-box relative z-10 top-0 left-0 right-0">
|
||||
<div class="absolute left-0 right-0 top-0 bottom-0 flex items-center flex-row">
|
||||
<div
|
||||
class="overflow-hidden flex h-full items-center flex-row flex-1 w-fit flex-shrink-0 min-w-0"
|
||||
>
|
||||
<div class="w-full flex items-center flex-row">
|
||||
<!-- 左边 -->
|
||||
<div
|
||||
v-if="designStore.isCollapse"
|
||||
class="left-box flex h-full items-center pl-20px gap-12px flex-shrink-0 flex-row"
|
||||
>
|
||||
<Collapse />
|
||||
<CreateChat />
|
||||
<div v-if="currentSession" class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
|
||||
</div>
|
||||
|
||||
<!-- 中间 -->
|
||||
<div class="middle-box flex-1 min-w-0 ml-12px">
|
||||
<TitleEditing />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row">
|
||||
<StartChatBtn />
|
||||
<AnnouncementBtn />
|
||||
<ModelLibraryBtn />
|
||||
<ThemeBtn />
|
||||
<AiTutorialBtn />
|
||||
<ConsoleBtn @open-console="handleOpenConsole" />
|
||||
<BuyBtn v-show="userStore.userInfo" />
|
||||
<Avatar v-show="userStore.userInfo" ref="avatarRef" />
|
||||
<LoginBtn v-show="!userStore.userInfo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
.header-box {
|
||||
width: 100%;
|
||||
width: calc(
|
||||
100% - var(--sidebar-left-container-default-width, 0px) - var(
|
||||
--sidebar-right-container-default-width,
|
||||
0px
|
||||
)
|
||||
);
|
||||
height: var(--header-container-default-heigth);
|
||||
margin: 0 var(--sidebar-right-container-default-width, 0) 0
|
||||
var(--sidebar-left-container-default-width, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
8
Yi.Ai.Vue3/src/layouts/components0/Logo/index.vue
Normal file
8
Yi.Ai.Vue3/src/layouts/components0/Logo/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<!-- Logo -->
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div>Logo</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
89
Yi.Ai.Vue3/src/layouts/components0/Main/index.vue
Normal file
89
Yi.Ai.Vue3/src/layouts/components0/Main/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<!-- Main -->
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import { useKeepAliveStore } from '@/stores/modules/keepAlive';
|
||||
|
||||
const designStore = useDesignStore();
|
||||
const keepAliveStore = useKeepAliveStore();
|
||||
const useroute = useRoute();
|
||||
|
||||
const transitionName = computed(() => {
|
||||
if (useroute.meta.isDefaultChat) {
|
||||
return 'slide';
|
||||
}
|
||||
else {
|
||||
return designStore.pageAnimateType;
|
||||
}
|
||||
});
|
||||
|
||||
// 刷新当前路由页面缓存方法
|
||||
const isRouterShow = ref(true);
|
||||
const refreshMainPage = (val: boolean) => (isRouterShow.value = val);
|
||||
provide('refresh', refreshMainPage);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-main
|
||||
class="layout-main"
|
||||
:class="{ 'layout-main-overfow-hidden': useroute.meta.isDefaultChat }"
|
||||
>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition :name="transitionName" mode="out-in" appear>
|
||||
<keep-alive :max="10" :include="keepAliveStore.keepAliveName">
|
||||
<component :is="Component" v-if="isRouterShow" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.layout-main-overfow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 默认聊天页面:上下滑动动画 */
|
||||
.slide-enter-from {
|
||||
margin-top: 200px;
|
||||
opacity: 0;
|
||||
}
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: all 0.3s; /* 缓出动画 */
|
||||
}
|
||||
.slide-enter-to {
|
||||
margin-top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
.slide-leave-from {
|
||||
margin-top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
.slide-leave-to {
|
||||
margin-top: 200px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 带id聊天页面:中间缩放动画 */
|
||||
// .zoom-fade-enter-from {
|
||||
// transform: scale(0.9); /* 进入前:缩小隐藏 */
|
||||
// opacity: 0;
|
||||
// }
|
||||
// .zoom-fade-enter-active,
|
||||
// .zoom-fade-leave-active {
|
||||
// transition: all 0.3s; /* 缓入动画 */
|
||||
// }
|
||||
// .zoom-fade-enter-to {
|
||||
// transform: scale(1); /* 进入后:正常大小 */
|
||||
// opacity: 1;
|
||||
// }
|
||||
// .zoom-fade-leave-from {
|
||||
// transform: scale(1); /* 离开前:正常大小 */
|
||||
// opacity: 1;
|
||||
// }
|
||||
// .zoom-fade-leave-to {
|
||||
// transform: scale(0.9); /* 离开后:缩小隐藏 */
|
||||
// opacity: 0;
|
||||
// }
|
||||
</style>
|
||||
@@ -1,21 +1,41 @@
|
||||
<!-- 主布局 -->
|
||||
<script setup lang="ts">
|
||||
import type { LayoutType } from '@/config/design';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useResponsive } from '@/hooks/useResponsive';
|
||||
import LayoutBlankPage from '@/layouts/LayoutBlankPage/index.vue';
|
||||
// import { useScreenStore } from '@/hooks/useScreen';
|
||||
import LayoutDefault from '@/layouts/LayoutDefault/index.vue';
|
||||
import LayoutMobile from '@/layouts/LayoutMobile/index.vue';
|
||||
import LayoutVertical from '@/layouts/LayoutVertical/index.vue';
|
||||
import { useDesignStore } from '@/stores';
|
||||
|
||||
// 这里添加布局类型
|
||||
const LayoutComponent: Record<LayoutType, Component> = {
|
||||
const LayoutComponent: Record<LayoutType | 'mobile', Component> = {
|
||||
default: LayoutDefault,
|
||||
vertical: LayoutVertical,
|
||||
blankPage: LayoutBlankPage,
|
||||
mobile: LayoutMobile,
|
||||
|
||||
};
|
||||
|
||||
const designStore = useDesignStore();
|
||||
// const { isMobile } = useScreenStore();
|
||||
const { isMobile } = useResponsive();
|
||||
const route = useRoute();
|
||||
|
||||
/** 获取布局格式 */
|
||||
const layout = computed((): LayoutType => designStore.layout);
|
||||
const layout = computed((): LayoutType | 'mobile' => {
|
||||
// 移动端强制使用移动布局
|
||||
// if (isMobile.value) {
|
||||
// return 'mobile';
|
||||
// }
|
||||
// 优先使用路由 meta 中定义的 layout
|
||||
if (route.meta?.layout) {
|
||||
return route.meta.layout as LayoutType;
|
||||
}
|
||||
// 否则使用全局设置的 layout
|
||||
return designStore.layout;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 更好的做法是等待所有资源加载
|
||||
window.addEventListener('load', () => {
|
||||
|
||||
226
Yi.Ai.Vue3/src/pages/chat/components/ConversationList.vue
Normal file
226
Yi.Ai.Vue3/src/pages/chat/components/ConversationList.vue
Normal file
@@ -0,0 +1,226 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { useSessionStore } from '@/stores';
|
||||
|
||||
const router = useRouter();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
// 会话列表加载状态
|
||||
const loading = computed(() => sessionStore.isLoading);
|
||||
const sessionList = computed(() => sessionStore.sessionList);
|
||||
|
||||
// 新建对话
|
||||
function handleNewChat() {
|
||||
router.push('/chat/conversation');
|
||||
}
|
||||
|
||||
// 选择对话
|
||||
function handleSelectSession(sessionId: string) {
|
||||
router.push(`/chat/conversation/${sessionId}`);
|
||||
}
|
||||
|
||||
// 删除对话
|
||||
async function handleDeleteSession(sessionId: string, sessionTitle: string) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除对话"${sessionTitle}"吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
|
||||
await sessionStore.deleteSessions([sessionId]);
|
||||
ElMessage.success('删除成功');
|
||||
|
||||
// 如果删除的是当前对话,跳转到新建页面
|
||||
if (router.currentRoute.value.params.id === sessionId) {
|
||||
router.push('/chat/conversation');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名对话
|
||||
async function handleRenameSession(sessionId: string, oldTitle: string) {
|
||||
try {
|
||||
const { value: newTitle } = await ElMessageBox.prompt('请输入新的对话名称', '重命名对话', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: oldTitle,
|
||||
inputPattern: /\S+/,
|
||||
inputErrorMessage: '对话名称不能为空',
|
||||
});
|
||||
|
||||
if (newTitle && newTitle !== oldTitle) {
|
||||
await sessionStore.updateSession({
|
||||
id: sessionId,
|
||||
title: newTitle,
|
||||
});
|
||||
ElMessage.success('重命名成功');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('重命名失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
function handleLoadMore() {
|
||||
sessionStore.loadMoreSessions();
|
||||
}
|
||||
|
||||
// 初始化加载
|
||||
onMounted(() => {
|
||||
if (sessionList.value.length === 0) {
|
||||
sessionStore.requestSessionList();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="conversation-list">
|
||||
<!-- 新建对话按钮 -->
|
||||
<div class="new-chat-btn">
|
||||
<el-button type="primary" size="large" style="width: 100%;" @click="handleNewChat">
|
||||
<el-icon><i-ep-plus /></el-icon>
|
||||
新建对话
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 对话列表 -->
|
||||
<div v-loading="loading" class="session-list">
|
||||
<div
|
||||
v-for="session in sessionList"
|
||||
:key="session.id"
|
||||
class="session-item"
|
||||
:class="{ active: $route.params.id === session.id }"
|
||||
@click="handleSelectSession(session.id)"
|
||||
>
|
||||
<div class="session-content">
|
||||
<div class="session-title">
|
||||
{{ session.title || '未命名对话' }}
|
||||
</div>
|
||||
<div class="session-time">
|
||||
{{ session.updateTime || session.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-actions">
|
||||
<el-dropdown trigger="click" @command="(cmd: string) => cmd === 'delete' ? handleDeleteSession(session.id, session.title) : handleRenameSession(session.id, session.title)">
|
||||
<el-button size="small" text circle @click.stop>
|
||||
<el-icon><i-ep-more-filled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="rename">
|
||||
<el-icon><i-ep-edit /></el-icon>
|
||||
重命名
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete">
|
||||
<el-icon><i-ep-delete /></el-icon>
|
||||
删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<div v-if="sessionStore.hasMore" class="load-more">
|
||||
<el-button text @click="handleLoadMore">
|
||||
加载更多
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-if="!loading && sessionList.length === 0" description="暂无对话记录" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversation-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 260px;
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.new-chat-btn {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.session-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.session-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.session-actions {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.session-item:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
}
|
||||
</style>
|
||||
44
Yi.Ai.Vue3/src/pages/chat/conversation/index.vue
Normal file
44
Yi.Ai.Vue3/src/pages/chat/conversation/index.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import ChatAside from '@/layouts/components/ChatAside/index.vue';
|
||||
import { useDesignStore } from '@/stores';
|
||||
import ChatDefault from '../layouts/chatDefaul/index.vue';
|
||||
import ChatWithId from '../layouts/chatWithId/index.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const designStore = useDesignStore();
|
||||
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
// const sessionId = true;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="conversation-page">
|
||||
<!-- 左侧对话列表 -->
|
||||
<!-- <ConversationList /> -->
|
||||
<!-- <ChatAside v-show="designStore.isCollapseConversationList" /> -->
|
||||
<ChatAside />
|
||||
<!-- 右侧聊天内容 -->
|
||||
<div class="chat-content">
|
||||
<ChatDefault v-if="!sessionId" />
|
||||
<ChatWithId v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversation-page {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--header-container-default-height));
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
26
Yi.Ai.Vue3/src/pages/chat/image/index.vue
Normal file
26
Yi.Ai.Vue3/src/pages/chat/image/index.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
// 图片生成功能 - 预留
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-generation-page">
|
||||
<el-empty description="图片生成功能开发中,敬请期待">
|
||||
<template #image>
|
||||
<el-icon style="font-size: 80px; color: var(--el-color-primary);">
|
||||
<i-ep-picture />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.image-generation-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +1,304 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import ChatDefaul from '@/pages/chat/layouts/chatDefaul/index.vue';
|
||||
import ChatWithId from '@/pages/chat/layouts/chatWithId/index.vue';
|
||||
import { Expand, Fold } from '@element-plus/icons-vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const sessionId = computed(() => route.params?.id);
|
||||
const router = useRouter();
|
||||
|
||||
// 控制侧边栏折叠状态
|
||||
const isCollapsed = ref(false);
|
||||
|
||||
// 菜单项配置
|
||||
const navItems = [
|
||||
{ name: 'conversation', label: '对话', icon: 'ChatDotRound', path: '/chat/conversation' },
|
||||
{ name: 'image', label: '图片生成', icon: 'Picture', path: '/chat/image' },
|
||||
{ name: 'video', label: '视频生成', icon: 'VideoCamera', path: '/chat/video' },
|
||||
];
|
||||
|
||||
// 当前激活的菜单
|
||||
const activeNav = computed(() => {
|
||||
const path = route.path;
|
||||
const item = navItems.find(item => item.path === path);
|
||||
return item?.name || 'user';
|
||||
});
|
||||
|
||||
// 切换菜单
|
||||
function handleNavSelect(menu: typeof navItems[0]) {
|
||||
router.push(menu.path);
|
||||
}
|
||||
|
||||
// 在移动端默认折叠侧边栏
|
||||
const isMobile = ref(false);
|
||||
function checkIsMobile() {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
// 移动端默认折叠
|
||||
if (isMobile.value) {
|
||||
isCollapsed.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始检查和监听窗口大小变化
|
||||
checkIsMobile();
|
||||
window.addEventListener('resize', checkIsMobile);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-container">
|
||||
<!-- 默认聊天页面 -->
|
||||
<ChatDefaul v-if="!sessionId" />
|
||||
<!-- 带id的聊天页面 -->
|
||||
<ChatWithId v-else />
|
||||
<div class="console-page" :class="{ 'is-collapsed': isCollapsed }">
|
||||
<!-- 侧边栏导航 -->
|
||||
<div class="nav-sidebar" :class="{ 'is-collapsed': isCollapsed }">
|
||||
<div class="nav-header">
|
||||
<h2 v-show="!isCollapsed" class="nav-title">
|
||||
AI聊天
|
||||
</h2>
|
||||
<div class="collapse-btn" @click="isCollapsed = !isCollapsed">
|
||||
<el-icon>
|
||||
<Expand v-if="isCollapsed" />
|
||||
<Fold v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeNav"
|
||||
class="nav-menu"
|
||||
:collapse="isCollapsed"
|
||||
:collapse-transition="false"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
:index="item.name"
|
||||
@click="handleNavSelect(item)"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<template #title>
|
||||
<span>{{ item.label }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- 折叠遮罩层(移动端) -->
|
||||
<div
|
||||
v-if="isMobile && !isCollapsed"
|
||||
class="sidebar-overlay"
|
||||
@click="isCollapsed = true"
|
||||
/>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="content-main">
|
||||
<div v-if="isMobile" class="content-header">
|
||||
<div class="mobile-toggle" @click="isCollapsed = false">
|
||||
<el-icon><i-ep-expand /></el-icon>
|
||||
<span>菜单</span>
|
||||
</div>
|
||||
</div>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-container {
|
||||
position: relative;
|
||||
<style scoped lang="scss">
|
||||
.console-page {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&.is-collapsed {
|
||||
.content-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
border-right: var(--header-border);
|
||||
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
|
||||
&.is-collapsed {
|
||||
width: 64px;
|
||||
|
||||
.nav-title {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-header {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 70px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// width: calc(100% - 32px);
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
overflow-anchor: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
border-right: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
&.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
span {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.el-menu--collapse) {
|
||||
:deep(.el-menu-item) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
flex: 1;
|
||||
//padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: none;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-primary);
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.console-page {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.is-collapsed {
|
||||
transform: translateX(-100%);
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
&:not(.is-collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
//padding: 15px;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 平板适配
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.nav-sidebar {
|
||||
width: 200px;
|
||||
|
||||
&.is-collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-main {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,87 +7,114 @@ import { ElMessage } from 'element-plus';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import ModelSelect from '@/components/ModelSelect/index.vue';
|
||||
import WelecomeText from '@/components/WelecomeText/index.vue';
|
||||
import { useGuideTourStore, useUserStore } from '@/stores';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useFilesStore } from '@/stores/modules/files';
|
||||
|
||||
import { useSessionStore } from '@/stores/modules/session';
|
||||
|
||||
// Store 实例
|
||||
const userStore = useUserStore();
|
||||
const sessionStore = useSessionStore();
|
||||
const filesStore = useFilesStore();
|
||||
const guideTourStore = useGuideTourStore();
|
||||
|
||||
const senderValue = ref('');
|
||||
const senderRef = ref();
|
||||
// 响应式数据
|
||||
const senderValue = ref(''); // 输入框内容
|
||||
const senderRef = ref(); // Sender 组件引用
|
||||
const isSending = ref(false); // 发送状态标志
|
||||
|
||||
// 防抖发送函数
|
||||
const debouncedSend = useDebounceFn(async () => {
|
||||
/**
|
||||
* 防抖发送消息函数
|
||||
*/
|
||||
const debouncedSend = useDebounceFn(
|
||||
async () => {
|
||||
// 1. 验证输入
|
||||
if (!senderValue.value.trim()) {
|
||||
ElMessage.warning('消息内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 检查是否正在发送
|
||||
if (isSending.value) {
|
||||
ElMessage.warning('请等待上一条消息发送完成');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = senderValue.value;
|
||||
// 3. 准备发送数据
|
||||
const content = senderValue.value.trim();
|
||||
isSending.value = true;
|
||||
|
||||
try {
|
||||
// 4. 保存到本地存储(可选,用于页面刷新后恢复)
|
||||
localStorage.setItem('chatContent', content);
|
||||
|
||||
// 5. 创建会话
|
||||
await sessionStore.createSessionList({
|
||||
userId: userStore.userInfo?.userId as number,
|
||||
sessionContent: content,
|
||||
sessionTitle: content.slice(0, 10),
|
||||
remark: content.slice(0, 10),
|
||||
});
|
||||
senderValue.value = ''; // 清空输入框
|
||||
|
||||
// 6. 清空输入框
|
||||
senderValue.value = '';
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('发送消息失败:', error);
|
||||
ElMessage.error(error);
|
||||
ElMessage.error(error.message || '发送消息失败');
|
||||
}
|
||||
finally {
|
||||
// 7. 重置发送状态
|
||||
isSending.value = false;
|
||||
}
|
||||
}, 800, { leading: true, trailing: false }); // 800ms防抖
|
||||
},
|
||||
800, // 防抖延迟
|
||||
{ leading: true, trailing: false }, // 立即执行第一次,忽略后续快速点击
|
||||
);
|
||||
|
||||
// 处理发送事件
|
||||
/**
|
||||
* 触发发送消息
|
||||
*/
|
||||
function handleSend() {
|
||||
debouncedSend();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件卡片
|
||||
* @param _item 文件项
|
||||
* @param index 文件索引
|
||||
*/
|
||||
function handleDeleteCard(_item: FilesCardProps, index: number) {
|
||||
filesStore.deleteFileByIndex(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听文件列表变化,自动展开/收起 Sender 头部
|
||||
*/
|
||||
watch(
|
||||
() => filesStore.filesList.length,
|
||||
(val) => {
|
||||
if (val > 0) {
|
||||
nextTick(() => {
|
||||
if (val > 0) {
|
||||
senderRef.value?.openHeader();
|
||||
});
|
||||
}
|
||||
else {
|
||||
nextTick(() => {
|
||||
senderRef.value?.closeHeader();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-defaul-wrap">
|
||||
<div class="chat-default">
|
||||
<div class="chat-default-wrap">
|
||||
<!-- 欢迎文本 -->
|
||||
<WelecomeText />
|
||||
|
||||
<!-- 消息发送器 -->
|
||||
<Sender
|
||||
ref="senderRef"
|
||||
v-model="senderValue"
|
||||
class="chat-defaul-sender"
|
||||
class="chat-default-sender"
|
||||
data-tour="chat-sender"
|
||||
:auto-size="{
|
||||
maxRows: 9,
|
||||
@@ -99,17 +126,19 @@ watch(
|
||||
:loading="isSending"
|
||||
@submit="handleSend"
|
||||
>
|
||||
<!-- 头部:文件附件区域 -->
|
||||
<template #header>
|
||||
<div class="sender-header p-12px pt-6px pb-0px">
|
||||
<div class="sender-header">
|
||||
<Attachments
|
||||
:items="filesStore.filesList"
|
||||
:hide-upload="true"
|
||||
@delete-card="handleDeleteCard"
|
||||
>
|
||||
<!-- 左侧滚动按钮 -->
|
||||
<template #prev-button="{ show, onScrollLeft }">
|
||||
<div
|
||||
v-if="show"
|
||||
class="prev-next-btn left-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
|
||||
class="scroll-btn prev-btn"
|
||||
@click="onScrollLeft"
|
||||
>
|
||||
<el-icon>
|
||||
@@ -118,10 +147,11 @@ watch(
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 右侧滚动按钮 -->
|
||||
<template #next-button="{ show, onScrollRight }">
|
||||
<div
|
||||
v-if="show"
|
||||
class="prev-next-btn right-8px flex-center w-22px h-22px rounded-8px border-1px border-solid border-[rgba(0,0,0,0.08)] c-[rgba(0,0,0,.4)] hover:bg-#f3f4f6 bg-#fff font-size-10px"
|
||||
class="scroll-btn next-btn"
|
||||
@click="onScrollRight"
|
||||
>
|
||||
<el-icon>
|
||||
@@ -132,37 +162,104 @@ watch(
|
||||
</Attachments>
|
||||
</div>
|
||||
</template>
|
||||
<template #prefix>
|
||||
<div class="flex-1 flex items-center gap-8px flex-none w-fit overflow-hidden">
|
||||
<FilesSelect />
|
||||
|
||||
<!-- 前缀:文件选择和模型选择 -->
|
||||
<template #prefix>
|
||||
<div class="sender-prefix">
|
||||
<FilesSelect />
|
||||
<ModelSelect />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 后缀:发送加载动画 -->
|
||||
<template #suffix>
|
||||
<el-icon v-if="isSending" class="is-loading">
|
||||
<el-icon v-if="isSending" class="loading-icon">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
</template>
|
||||
</Sender>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-defaul-wrap {
|
||||
.chat-default {
|
||||
width: 100%;
|
||||
//background: #ff11f3;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
//padding: 0 0 100px;
|
||||
}
|
||||
.chat-default-wrap {
|
||||
//background: #0bdcb7;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
min-height: 450px;
|
||||
.chat-defaul-sender {
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.chat-default-sender {
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-icon.is-loading) {
|
||||
.sender-header {
|
||||
padding: 12px 12px 0 12px;
|
||||
}
|
||||
|
||||
.sender-prefix {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: none;
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
background-color: #fff;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&.prev-btn {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
&.next-btn {
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-primary);
|
||||
animation: rotating 2s linear infinite;
|
||||
@@ -176,4 +273,21 @@ watch(
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.chat-default-wrap {
|
||||
padding: 12px;
|
||||
min-height: calc(100vh - 120px);
|
||||
|
||||
.chat-default-sender {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.sender-prefix {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -562,14 +562,16 @@ function handleImagePreview(url: string) {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-with-id-container {
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
.chat-warp {
|
||||
max-width: 1000px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
26
Yi.Ai.Vue3/src/pages/chat/video/index.vue
Normal file
26
Yi.Ai.Vue3/src/pages/chat/video/index.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
// 视频生成功能 - 预留
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="video-generation-page">
|
||||
<el-empty description="视频生成功能开发中,敬请期待">
|
||||
<template #image>
|
||||
<el-icon style="font-size: 80px; color: var(--el-color-primary);">
|
||||
<i-ep-video-camera />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.video-generation-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
</style>
|
||||
313
Yi.Ai.Vue3/src/pages/console/index.vue
Normal file
313
Yi.Ai.Vue3/src/pages/console/index.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<script setup lang="ts">
|
||||
import { Expand, Fold } from '@element-plus/icons-vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 控制侧边栏折叠状态
|
||||
const isCollapsed = ref(false);
|
||||
|
||||
// 菜单项配置
|
||||
const navItems = [
|
||||
{ name: 'user', label: '用户信息', icon: 'User', path: '/console/user' },
|
||||
{ name: 'apikey', label: 'API密钥', icon: 'Key', path: '/console/apikey' },
|
||||
{ name: 'recharge-log', label: '充值记录', icon: 'Document', path: '/console/recharge-log' },
|
||||
{ name: 'usage', label: '用量统计', icon: 'Histogram', path: '/console/usage' },
|
||||
{ name: 'premium', label: '尊享服务', icon: 'ColdDrink', path: '/console/premium' },
|
||||
{ name: 'daily-task', label: '每日任务(限时)', icon: 'Trophy', path: '/console/daily-task' },
|
||||
{ name: 'invite', label: '每周邀请(限时)', icon: 'Present', path: '/console/invite' },
|
||||
{ name: 'activation', label: '激活码兑换', icon: 'MagicStick', path: '/console/activation' },
|
||||
];
|
||||
|
||||
// 当前激活的菜单
|
||||
const activeNav = computed(() => {
|
||||
const path = route.path;
|
||||
const item = navItems.find(item => item.path === path);
|
||||
return item?.name || 'user';
|
||||
});
|
||||
|
||||
// 切换菜单
|
||||
function handleNavSelect(menu: typeof navItems[0]) {
|
||||
router.push(menu.path);
|
||||
}
|
||||
|
||||
// 在移动端默认折叠侧边栏
|
||||
const isMobile = ref(false);
|
||||
function checkIsMobile() {
|
||||
isMobile.value = window.innerWidth <= 768;
|
||||
// 移动端默认折叠
|
||||
if (isMobile.value) {
|
||||
isCollapsed.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始检查和监听窗口大小变化
|
||||
checkIsMobile();
|
||||
window.addEventListener('resize', checkIsMobile);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="console-page" :class="{ 'is-collapsed': isCollapsed }">
|
||||
<!-- 侧边栏导航 -->
|
||||
<div class="nav-sidebar" :class="{ 'is-collapsed': isCollapsed }">
|
||||
<div class="nav-header">
|
||||
<h2 v-show="!isCollapsed" class="nav-title">
|
||||
控制台
|
||||
</h2>
|
||||
<div class="collapse-btn" @click="isCollapsed = !isCollapsed">
|
||||
<el-icon>
|
||||
<Expand v-if="isCollapsed" />
|
||||
<Fold v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeNav"
|
||||
class="nav-menu"
|
||||
:collapse="isCollapsed"
|
||||
:collapse-transition="false"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
:index="item.name"
|
||||
@click="handleNavSelect(item)"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="item.icon" />
|
||||
</el-icon>
|
||||
<template #title>
|
||||
<span>{{ item.label }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- 折叠遮罩层(移动端) -->
|
||||
<div
|
||||
v-if="isMobile && !isCollapsed"
|
||||
class="sidebar-overlay"
|
||||
@click="isCollapsed = true"
|
||||
/>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="content-main">
|
||||
<div v-if="isMobile" class="content-header">
|
||||
<div class="mobile-toggle" @click="isCollapsed = false">
|
||||
<el-icon><i-ep-expand /></el-icon>
|
||||
<span>菜单</span>
|
||||
</div>
|
||||
</div>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.console-page {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&.is-collapsed {
|
||||
.content-main {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
border-right: var(--header-border);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
|
||||
&.is-collapsed {
|
||||
width: 64px;
|
||||
|
||||
.nav-title {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-header {
|
||||
padding: 20px;
|
||||
//border-bottom:var(--header-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 70px;
|
||||
box-sizing: border-box;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
border-right: none;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
&.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
span {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.el-menu--collapse) {
|
||||
:deep(.el-menu-item) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
transition: margin-left 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: none;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--el-fill-color-light);
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-primary);
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.console-page {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&.is-collapsed {
|
||||
transform: translateX(-100%);
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
&:not(.is-collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
padding: 15px;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 平板适配
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.nav-sidebar {
|
||||
width: 200px;
|
||||
|
||||
&.is-collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-main {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -266,6 +266,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="false"
|
||||
:icon="HomeFilled"
|
||||
class="home-btn"
|
||||
round
|
||||
@@ -536,8 +537,8 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.model-library-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f5f7fa 0%, #ffffff 100%);
|
||||
overflow: auto;
|
||||
|
||||
// 顶部横幅
|
||||
.banner-section {
|
||||
@@ -570,7 +571,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
max-width: 1400px;
|
||||
//max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -686,7 +687,7 @@ onMounted(() => {
|
||||
padding: 32px 16px;
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 100%;
|
||||
//max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
|
||||
@@ -5,19 +5,20 @@ import { ROUTER_WHITE_LIST } from '@/config';
|
||||
import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter';
|
||||
import { useDesignStore, useUserStore } from '@/stores';
|
||||
|
||||
// 创建页面加载进度条,提升用户体验。
|
||||
const { start, done } = useNProgress(0, {
|
||||
showSpinner: false,
|
||||
trickleSpeed: 200,
|
||||
minimum: 0.3,
|
||||
easing: 'ease',
|
||||
speed: 500,
|
||||
showSpinner: false, // 不显示旋转器
|
||||
trickleSpeed: 200, // 进度条增长速度(毫秒)
|
||||
minimum: 0.3, // 最小进度值(30%)
|
||||
easing: 'ease', // 动画缓动函数
|
||||
speed: 500, // 动画速度
|
||||
});
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [...layoutRouter, ...staticRouter, ...errorRouter],
|
||||
strict: false,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
history: createWebHistory(), // 使用 HTML5 History 模式
|
||||
routes: [...layoutRouter, ...staticRouter, ...errorRouter], // 合并所有路由
|
||||
strict: false, // 不严格匹配尾部斜杠
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }), // 路由切换时滚动到顶部
|
||||
});
|
||||
|
||||
// 路由前置守卫
|
||||
@@ -27,14 +28,16 @@ router.beforeEach(
|
||||
_from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
) => {
|
||||
// 1. 获取状态管理
|
||||
const userStore = useUserStore();
|
||||
const designStore = useDesignStore(); // 必须在守卫内部调用
|
||||
designStore._setLayout(to.meta?.layout || 'vertical');
|
||||
// 2. 设置布局(根据路由meta中的layout配置)
|
||||
designStore._setLayout(to.meta?.layout || 'default');
|
||||
|
||||
// 1、NProgress 开始
|
||||
// 3. 开始显示进度条
|
||||
start();
|
||||
|
||||
// 2、标题
|
||||
// 4. 设置页面标题
|
||||
document.title = (to.meta.title as string) || (import.meta.env.VITE_WEB_TITLE as string);
|
||||
|
||||
// 3、权限 预留
|
||||
@@ -52,16 +55,17 @@ router.beforeEach(
|
||||
// return next();
|
||||
// }
|
||||
// 4、判断访问页面是否在路由白名单地址[静态路由]中,如果存在直接放行。
|
||||
// 5. 白名单检查(跳过权限验证)
|
||||
if (ROUTER_WHITE_LIST.includes(to.path))
|
||||
return next();
|
||||
|
||||
// 5、判断是否有 Token,没有重定向到 login 页面。
|
||||
// 6. Token 检查(用户认证),没有重定向到 login 页面。
|
||||
if (!userStore.token)
|
||||
userStore.logout();
|
||||
|
||||
// 其余逻辑 预留...
|
||||
|
||||
// 6、正常访问页面。
|
||||
// 8. 放行路由
|
||||
next();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,38 +1,72 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { HOME_URL } from '@/config';
|
||||
|
||||
// LayoutRouter[布局路由]
|
||||
export const layoutRouter: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: HOME_URL,
|
||||
component: () => import('@/layouts/index.vue'),
|
||||
children: [
|
||||
// 将首页重定向逻辑放在这里
|
||||
{
|
||||
path: HOME_URL,
|
||||
path: '',
|
||||
redirect: '/chat/conversation',
|
||||
},
|
||||
|
||||
// chat 路由组 - 修正路径和重定向
|
||||
{
|
||||
path: 'chat',
|
||||
name: 'chat',
|
||||
component: () => import('@/pages/chat/index.vue'),
|
||||
meta: {
|
||||
// title: '通用聊天页面',
|
||||
isDefaultChat: true,
|
||||
title: 'AI聊天',
|
||||
icon: 'HomeFilled',
|
||||
// isHide: '1', // 是否在菜单中隐藏[0是,1否] 预留
|
||||
// isKeepAlive: '0', // 是否缓存路由数据[0是,1否] 预留
|
||||
// isFull: '1', // 是否全屏[0是,1否] 预留
|
||||
// enName: "Master Station", // 英文名称 预留
|
||||
},
|
||||
children: [
|
||||
// chat 根路径重定向到 conversation
|
||||
{
|
||||
path: '',
|
||||
redirect: '/chat/conversation',
|
||||
},
|
||||
{
|
||||
path: 'conversation',
|
||||
name: 'chatConversation',
|
||||
component: () => import('@/pages/chat/conversation/index.vue'),
|
||||
meta: {
|
||||
title: 'AI对话',
|
||||
isDefaultChat: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/chat/:id',
|
||||
name: 'chatWithId',
|
||||
component: () => import('@/pages/chat/index.vue'),
|
||||
path: 'conversation/:id',
|
||||
name: 'chatConversationWithId',
|
||||
component: () => import('@/pages/chat/conversation/index.vue'),
|
||||
meta: {
|
||||
// title: '带 ID 的聊天页面',
|
||||
title: 'AI对话',
|
||||
isDefaultChat: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/products',
|
||||
path: 'image',
|
||||
name: 'chatImage',
|
||||
component: () => import('@/pages/chat/image/index.vue'),
|
||||
meta: {
|
||||
title: '图片生成',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'video',
|
||||
name: 'chatVideo',
|
||||
component: () => import('@/pages/chat/video/index.vue'),
|
||||
meta: {
|
||||
title: '视频生成',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// 产品页面
|
||||
{
|
||||
path: 'products',
|
||||
name: 'products',
|
||||
component: () => import('@/pages/products/index.vue'),
|
||||
meta: {
|
||||
@@ -41,33 +75,37 @@ export const layoutRouter: RouteRecordRaw[] = [
|
||||
isDefaultChat: false,
|
||||
layout: 'blankPage',
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
// 模型库
|
||||
{
|
||||
path: '/model-library',
|
||||
path: 'model-library',
|
||||
name: 'modelLibrary',
|
||||
component: () => import('@/pages/modelLibrary/index.vue'),
|
||||
meta: {
|
||||
title: '模型库',
|
||||
keepAlive: 0,
|
||||
isDefaultChat: false,
|
||||
layout: 'blankPage',
|
||||
layout: 'default',
|
||||
},
|
||||
},
|
||||
|
||||
// 支付结果
|
||||
{
|
||||
path: '/pay-result',
|
||||
path: 'pay-result',
|
||||
name: 'payResult',
|
||||
component: () => import('@/pages/payResult/index.vue'),
|
||||
meta: {
|
||||
title: '支付结果',
|
||||
keepAlive: 0, // 如果需要缓存
|
||||
isDefaultChat: false, // 根据实际情况设置
|
||||
layout: 'blankPage', // 如果需要自定义布局
|
||||
keepAlive: 0,
|
||||
isDefaultChat: false,
|
||||
layout: 'blankPage',
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
// 活动详情
|
||||
{
|
||||
path: '/activity/:id',
|
||||
path: 'activity/:id',
|
||||
name: 'activityDetail',
|
||||
component: () => import('@/pages/activity/detail.vue'),
|
||||
meta: {
|
||||
@@ -76,8 +114,10 @@ export const layoutRouter: RouteRecordRaw[] = [
|
||||
layout: 'blankPage',
|
||||
},
|
||||
},
|
||||
|
||||
// 公告详情
|
||||
{
|
||||
path: '/announcement/:id',
|
||||
path: 'announcement/:id',
|
||||
name: 'announcementDetail',
|
||||
component: () => import('@/pages/announcement/detail.vue'),
|
||||
meta: {
|
||||
@@ -86,11 +126,92 @@ export const layoutRouter: RouteRecordRaw[] = [
|
||||
layout: 'blankPage',
|
||||
},
|
||||
},
|
||||
|
||||
// 控制台路由组 - 修正路径和重定向
|
||||
{
|
||||
path: 'console',
|
||||
name: 'console',
|
||||
component: () => import('@/pages/console/index.vue'),
|
||||
meta: {
|
||||
title: '控制台',
|
||||
icon: 'Setting',
|
||||
layout: 'default',
|
||||
},
|
||||
children: [
|
||||
// console 根路径重定向到 user
|
||||
{
|
||||
path: '',
|
||||
redirect: '/console/user',
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
name: 'consoleUser',
|
||||
component: () => import('@/components/userPersonalCenter/components/UserManagement.vue'),
|
||||
meta: {
|
||||
title: '用户信息',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'apikey',
|
||||
name: 'consoleApikey',
|
||||
component: () => import('@/components/userPersonalCenter/components/APIKeyManagement.vue'),
|
||||
meta: {
|
||||
title: 'API密钥',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'recharge-log',
|
||||
name: 'consoleRechargeLog',
|
||||
component: () => import('@/components/userPersonalCenter/components/RechargeLog.vue'),
|
||||
meta: {
|
||||
title: '充值记录',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'usage',
|
||||
name: 'consoleUsage',
|
||||
component: () => import('@/components/userPersonalCenter/components/UsageStatistics.vue'),
|
||||
meta: {
|
||||
title: '用量统计',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'premium',
|
||||
name: 'consolePremium',
|
||||
component: () => import('@/components/userPersonalCenter/components/PremiumService.vue'),
|
||||
meta: {
|
||||
title: '尊享服务',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'daily-task',
|
||||
name: 'consoleDailyTask',
|
||||
component: () => import('@/components/userPersonalCenter/components/DailyTask.vue'),
|
||||
meta: {
|
||||
title: '每日任务',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'invite',
|
||||
name: 'consoleInvite',
|
||||
component: () => import('@/components/userPersonalCenter/components/CardFlipActivity.vue'),
|
||||
meta: {
|
||||
title: '每周邀请',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'activation',
|
||||
name: 'consoleActivation',
|
||||
component: () => import('@/components/userPersonalCenter/components/ActivationCode.vue'),
|
||||
meta: {
|
||||
title: '激活码兑换',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
// staticRouter[静态路由] 预留
|
||||
export const staticRouter: RouteRecordRaw[] = [];
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ const {
|
||||
pageAnimateType: rePageAnimateType,
|
||||
layout: reLayout,
|
||||
collapseType: reCollapseType,
|
||||
isCollapse: reisCollapse,
|
||||
isSafeAreaHover: reisSafeAreaHover,
|
||||
hasActivatedHover: rehasActivatedHover,
|
||||
isCollapse: reIsCollapse,
|
||||
isCollapseConversationList: reIsCollapseConversationList,
|
||||
|
||||
} = designSetting;
|
||||
|
||||
export const useDesignStore = defineStore(
|
||||
@@ -44,41 +44,22 @@ export const useDesignStore = defineStore(
|
||||
};
|
||||
|
||||
// 最终是否展开左侧菜单
|
||||
const isCollapse = ref<boolean>(reisCollapse);
|
||||
|
||||
const isCollapse = ref<boolean>(reIsCollapse);
|
||||
const setCollapse = (collapseFinal: boolean) => {
|
||||
console.log('c---', collapseFinal);
|
||||
isCollapse.value = collapseFinal;
|
||||
};
|
||||
|
||||
// 折叠按钮是否被悬停
|
||||
const isSafeAreaHover = ref<boolean>(reisSafeAreaHover);
|
||||
|
||||
const setSafeAreaHover = (hover: boolean) => {
|
||||
isSafeAreaHover.value = hover;
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value;
|
||||
};
|
||||
// 新- 对话列表折叠
|
||||
const isCollapseConversationList = ref<boolean>(reIsCollapseConversationList);
|
||||
const setIsCollapseConversationList = (collapseFinal: boolean) => {
|
||||
isCollapseConversationList.value = collapseFinal;
|
||||
};
|
||||
const toggleCollapseConversationList = () => {
|
||||
isCollapseConversationList.value = !isCollapseConversationList.value;
|
||||
};
|
||||
|
||||
// 跟踪是否首次激活悬停
|
||||
const hasActivatedHover = ref<boolean>(rehasActivatedHover);
|
||||
|
||||
// 两个监听不要合并
|
||||
watch(
|
||||
() => isCollapse.value,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
hasActivatedHover.value = false;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => isSafeAreaHover.value,
|
||||
() => {
|
||||
hasActivatedHover.value = true;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
return {
|
||||
darkMode,
|
||||
setDarkMode,
|
||||
@@ -92,10 +73,11 @@ export const useDesignStore = defineStore(
|
||||
setCollapseType,
|
||||
isCollapse,
|
||||
setCollapse,
|
||||
isSafeAreaHover,
|
||||
setSafeAreaHover,
|
||||
hasActivatedHover,
|
||||
_setLayout,
|
||||
toggleCollapse,
|
||||
isCollapseConversationList,
|
||||
setIsCollapseConversationList,
|
||||
toggleCollapseConversationList,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@ export const useSessionStore = defineStore('session', () => {
|
||||
try {
|
||||
// 清空当前选中会话信息
|
||||
setCurrentSession(null);
|
||||
router.replace({ name: 'chat' });
|
||||
router.replace({ name: 'chatConversationWithId' });
|
||||
}
|
||||
catch (error) {
|
||||
console.error('createSessionBtn错误:', error);
|
||||
@@ -109,7 +109,7 @@ export const useSessionStore = defineStore('session', () => {
|
||||
const createSessionList = async (data: Omit<CreateSessionDTO, 'id'>) => {
|
||||
if (!userStore.token) {
|
||||
router.replace({
|
||||
name: 'chatWithId',
|
||||
name: 'chatConversationWithId',
|
||||
params: {
|
||||
id: 'not_login',
|
||||
},
|
||||
@@ -140,7 +140,7 @@ export const useSessionStore = defineStore('session', () => {
|
||||
setCurrentSession(newSessionRes.data);
|
||||
// 跳转聊天页
|
||||
router.replace({
|
||||
name: 'chatWithId',
|
||||
name: 'chatConversationWithId',
|
||||
params: { id: `${res.data.id}` },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const useUserStore = defineStore(
|
||||
// 如果需要调用接口,可以在这里调用
|
||||
clearToken();
|
||||
clearUserInfo();
|
||||
router.replace({ name: 'chat' });
|
||||
router.replace({ name: 'chatConversationWithId' });
|
||||
};
|
||||
|
||||
// 新增:登录弹框状态
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
@use './elx';
|
||||
@use './guide-tour';
|
||||
body{
|
||||
overflow: hidden;
|
||||
//overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,69 @@
|
||||
/* 定义全局变量 */
|
||||
:root {
|
||||
/* 头部高度 */
|
||||
--header-container-default-heigth: 56px;
|
||||
|
||||
/* 左侧侧边栏背景色 */
|
||||
/* ========== 颜色系统 ========== */
|
||||
/* 主色调 */
|
||||
--color-primary: #3b82f6;
|
||||
--color-primary-light: #60a5fa;
|
||||
--color-primary-lighter: #93c5fd;
|
||||
--color-primary-dark: #2563eb;
|
||||
--color-primary-darker: #1d4ed8;
|
||||
|
||||
/* 辅助色 */
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-danger: #ef4444;
|
||||
--color-info: #6b7280;
|
||||
|
||||
/* 中性色 */
|
||||
--color-white: #ffffff;
|
||||
--color-black: #000000;
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
/* ========== 头部区域 ========== */
|
||||
--header-container-default-height: 60px;
|
||||
--header-background-color: #ffffff;
|
||||
--header-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
--header-text-color: #191919;
|
||||
--header-icon-color: #666666;
|
||||
--header-border: 1px solid var(--color-gray-300);
|
||||
|
||||
/* ========== 侧边栏区域 ========== */
|
||||
--sidebar-background-color: #f3f4f6;
|
||||
--sidebar-default-width: 240px;
|
||||
--sidebar-collapsed-width: 64px;
|
||||
--sidebar-active-bg-color: #e5e7eb;
|
||||
--sidebar-active-text-color: #111827;
|
||||
--sidebar-hover-bg-color: #e5e7eb;
|
||||
--sidebar-text-color: #374151;
|
||||
--sidebar-icon-color: #6b7280;
|
||||
--sidebar-border-color: rgba(0, 0, 0, 0.08);
|
||||
|
||||
/* ========== 主要内容区域 ========== */
|
||||
--content-background-color: #ffffff;
|
||||
--content-padding: 20px;
|
||||
--content-max-width: 1200px;
|
||||
|
||||
/* 登录弹框变量 */
|
||||
/* ========== 聊天区域 ========== */
|
||||
--chat-container-bg: #ffffff;
|
||||
--chat-sender-bg: #ffffff;
|
||||
--chat-sender-border: 1px solid #e5e7eb;
|
||||
--chat-bubble-user-bg: #3b82f6;
|
||||
--chat-bubble-user-text: #ffffff;
|
||||
--chat-bubble-ai-bg: #f3f4f6;
|
||||
--chat-bubble-ai-text: #374151;
|
||||
--chat-typing-indicator-color: #9ca3af;
|
||||
|
||||
/* ========== 登录弹框 ========== */
|
||||
--login-dialog-width: 738px;
|
||||
--login-dialog-height: 416px;
|
||||
--login-dialog-padding: 0px;
|
||||
@@ -15,10 +72,179 @@
|
||||
--login-dialog-mode-toggle-color: #409eff;
|
||||
--login-dialog-logo-background: #ffffff;
|
||||
--login-dialog-logo-text-color: #191919;
|
||||
--login-dialog-form-bg: #ffffff;
|
||||
--login-dialog-form-padding: 40px;
|
||||
--login-dialog-input-border: 1px solid #e5e7eb;
|
||||
--login-dialog-input-focus-border: 1px solid #3b82f6;
|
||||
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-color-primary: #111827;
|
||||
--text-color-secondary: #374151;
|
||||
--text-color-tertiary: #6b7280;
|
||||
--text-color-placeholder: #9ca3af;
|
||||
--text-color-disabled: #d1d5db;
|
||||
--text-color-inverse: #ffffff;
|
||||
|
||||
/* 覆盖 element-plus 样式 */
|
||||
/* 背景颜色 */
|
||||
--bg-color-primary: #ffffff;
|
||||
--bg-color-secondary: #f9fafb;
|
||||
--bg-color-tertiary: #f3f4f6;
|
||||
--bg-color-overlay: rgba(0, 0, 0, 0.5);
|
||||
--bg-color-mask: rgba(0, 0, 0, 0.45);
|
||||
|
||||
/* ========== 间距系统 ========== */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 12px;
|
||||
--spacing-lg: 16px;
|
||||
--spacing-xl: 20px;
|
||||
--spacing-2xl: 24px;
|
||||
--spacing-3xl: 32px;
|
||||
--spacing-4xl: 40px;
|
||||
--spacing-5xl: 48px;
|
||||
--spacing-6xl: 64px;
|
||||
|
||||
/* ========== 边框 ========== */
|
||||
--border-radius-none: 0px;
|
||||
--border-radius-sm: 4px;
|
||||
--border-radius-md: 8px;
|
||||
--border-radius-lg: 12px;
|
||||
--border-radius-xl: 16px;
|
||||
--border-radius-2xl: 24px;
|
||||
--border-radius-full: 9999px;
|
||||
|
||||
--border-width-thin: 1px;
|
||||
--border-width-thick: 2px;
|
||||
|
||||
--border-color-light: #e5e7eb;
|
||||
--border-color-default: #d1d5db;
|
||||
--border-color-dark: #9ca3af;
|
||||
|
||||
/* ========== 阴影系统 ========== */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* ========== 字体系统 ========== */
|
||||
--font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-mono: 'SF Mono', Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', monospace;
|
||||
|
||||
--font-size-xs: 12px;
|
||||
--font-size-sm: 14px;
|
||||
--font-size-base: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 24px;
|
||||
--font-size-3xl: 30px;
|
||||
--font-size-4xl: 36px;
|
||||
--font-size-5xl: 48px;
|
||||
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.75;
|
||||
--line-height-loose: 2;
|
||||
|
||||
/* ========== 动效 ========== */
|
||||
--transition-fast: 150ms;
|
||||
--transition-normal: 250ms;
|
||||
--transition-slow: 350ms;
|
||||
--transition-very-slow: 500ms;
|
||||
|
||||
--transition-timing-linear: cubic-bezier(0, 0, 1, 1);
|
||||
--transition-timing-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-timing-ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--transition-timing-ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--transition-timing-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* ========== Z-index 层级 ========== */
|
||||
--z-index-dropdown: 1000;
|
||||
--z-index-sticky: 1020;
|
||||
--z-index-fixed: 1030;
|
||||
--z-index-modal-backdrop: 1040;
|
||||
--z-index-modal: 1050;
|
||||
--z-index-popover: 1060;
|
||||
--z-index-tooltip: 1070;
|
||||
--z-index-toast: 1080;
|
||||
--z-index-max: 9999;
|
||||
|
||||
/* ========== 其他 ========== */
|
||||
--container-max-width: 1200px;
|
||||
--container-padding: 16px;
|
||||
|
||||
--input-height: 40px;
|
||||
--input-padding: 12px;
|
||||
--button-height: 40px;
|
||||
--button-padding: 12px 24px;
|
||||
|
||||
/* ========== 覆盖 element-plus 样式 ========== */
|
||||
--el-border-radius-base: 12px !important;
|
||||
--el-messagebox-border-radius: 16px !important;
|
||||
--el-color-primary: var(--color-primary) !important;
|
||||
--el-color-success: var(--color-success) !important;
|
||||
--el-color-warning: var(--color-warning) !important;
|
||||
--el-color-danger: var(--color-danger) !important;
|
||||
--el-color-info: var(--color-info) !important;
|
||||
--el-font-size-base: var(--font-size-base) !important;
|
||||
--el-font-family: var(--font-family-sans) !important;
|
||||
|
||||
/* Element Plus 组件特定变量 */
|
||||
--el-menu-item-height: 48px;
|
||||
--el-menu-bg-color: var(--sidebar-background-color);
|
||||
--el-menu-text-color: var(--text-color-secondary);
|
||||
--el-menu-active-color: var(--color-primary);
|
||||
--el-menu-hover-bg-color: var(--color-gray-100);
|
||||
|
||||
/* 表单相关 */
|
||||
--el-form-label-font-size: var(--font-size-sm);
|
||||
--el-input-height: var(--input-height);
|
||||
--el-input-border-color: var(--border-color-light);
|
||||
--el-input-hover-border-color: var(--color-primary-light);
|
||||
--el-input-focus-border-color: var(--color-primary);
|
||||
|
||||
/* 按钮相关 */
|
||||
--el-button-border-radius-base: var(--border-radius-md);
|
||||
--el-button-hover-bg-color: var(--color-primary-dark);
|
||||
--el-button-active-bg-color: var(--color-primary-darker);
|
||||
}
|
||||
|
||||
/* ========== 暗色模式变量 ========== */
|
||||
[data-theme="dark"] {
|
||||
--sidebar-background-color: #1f2937;
|
||||
--header-background-color: #111827;
|
||||
--content-background-color: #111827;
|
||||
--chat-container-bg: #111827;
|
||||
--chat-sender-bg: #1f2937;
|
||||
--chat-bubble-ai-bg: #374151;
|
||||
|
||||
--text-color-primary: #f9fafb;
|
||||
--text-color-secondary: #e5e7eb;
|
||||
--text-color-tertiary: #9ca3af;
|
||||
|
||||
--bg-color-primary: #111827;
|
||||
--bg-color-secondary: #1f2937;
|
||||
--bg-color-tertiary: #374151;
|
||||
|
||||
--border-color-light: #374151;
|
||||
--border-color-default: #4b5563;
|
||||
--border-color-dark: #6b7280;
|
||||
}
|
||||
|
||||
/* ========== 响应式断点 ========== */
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--content-padding: 12px;
|
||||
--container-padding: 12px;
|
||||
--sidebar-default-width: 100%;
|
||||
--sidebar-collapsed-width: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
line-height: 28px !important;
|
||||
}
|
||||
.chat-with-id-container{
|
||||
max-width: 1000px !important;
|
||||
//待定
|
||||
//max-width: 1000px !important;
|
||||
}
|
||||
.el-bubble-content-wrapper {
|
||||
--bubble-content-max-width: 800px;
|
||||
|
||||
3
Yi.Ai.Vue3/types/components.d.ts
vendored
3
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -25,8 +25,6 @@ declare module 'vue' {
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
@@ -49,6 +47,7 @@ declare module 'vue' {
|
||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
|
||||
Reference in New Issue
Block a user