fix: 前端页面架构重构初版

This commit is contained in:
Gsh
2025-12-28 22:42:17 +08:00
parent 4b9f845fae
commit 411a9058ca
53 changed files with 6098 additions and 845 deletions

View File

@@ -2,7 +2,8 @@
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(npx vue-tsc --noEmit)", "Bash(npx vue-tsc --noEmit)",
"Bash(timeout 60 npx vue-tsc:*)" "Bash(timeout 60 npx vue-tsc:*)",
"Bash(npm run dev:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -549,6 +549,7 @@ onBeforeUnmount(() => {
</el-option> </el-option>
</el-select> </el-select>
<el-button <el-button
v-if="false"
:icon="FullScreen" :icon="FullScreen"
circle circle
plain plain

View File

@@ -105,6 +105,8 @@ function bindWechat() {
<template> <template>
<div class="user-profile"> <div class="user-profile">
<!-- 用户卡片 -->
<el-card class="profile-card" shadow="hover">
<!-- 顶部标题 --> <!-- 顶部标题 -->
<div class="header"> <div class="header">
<h2> <h2>
@@ -112,9 +114,6 @@ function bindWechat() {
个人信息 个人信息
</h2> </h2>
</div> </div>
<!-- 用户卡片 -->
<el-card class="profile-card" shadow="hover">
<!-- 头像和基本信息区域 --> <!-- 头像和基本信息区域 -->
<div class="user-header-section"> <div class="user-header-section">
<!-- 头像区域 --> <!-- 头像区域 -->
@@ -138,7 +137,9 @@ function bindWechat() {
<!-- 用户名称和状态 --> <!-- 用户名称和状态 -->
<div class="user-info-quick"> <div class="user-info-quick">
<h3 class="user-name">{{ userNick }}</h3> <h3 class="user-name">
{{ userNick }}
</h3>
<div class="user-tags"> <div class="user-tags">
<el-tag v-if="userVipStatus" type="warning" effect="dark" size="large"> <el-tag v-if="userVipStatus" type="warning" effect="dark" size="large">
<el-icon><Promotion /></el-icon> <el-icon><Promotion /></el-icon>
@@ -153,8 +154,12 @@ function bindWechat() {
</div> </div>
<div class="user-stats"> <div class="user-stats">
<div class="stat-item"> <div class="stat-item">
<div class="stat-value">{{ formatDate(user.creationTime)?.split(' ')[0] || '-' }}</div> <div class="stat-value">
<div class="stat-label">注册时间</div> {{ formatDate(user.creationTime)?.split(' ')[0] || '-' }}
</div>
<div class="stat-label">
注册时间
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -171,7 +176,9 @@ function bindWechat() {
<el-icon><User /></el-icon> <el-icon><User /></el-icon>
用户名 用户名
</div> </div>
<div class="info-value">{{ user.userName || '-' }}</div> <div class="info-value">
{{ user.userName || '-' }}
</div>
</div> </div>
<!-- 昵称 --> <!-- 昵称 -->
@@ -180,7 +187,9 @@ function bindWechat() {
<el-icon><Postcard /></el-icon> <el-icon><Postcard /></el-icon>
昵称 昵称
</div> </div>
<div class="info-value">{{ userNick }}</div> <div class="info-value">
{{ userNick }}
</div>
</div> </div>
<!-- 邮箱 --> <!-- 邮箱 -->
@@ -215,7 +224,9 @@ function bindWechat() {
<!-- 微信绑定 --> <!-- 微信绑定 -->
<div class="info-item full-width"> <div class="info-item full-width">
<div class="info-label"> <div class="info-label">
<el-icon color="#07C160"><ChatDotRound /></el-icon> <el-icon color="#07C160">
<ChatDotRound />
</el-icon>
微信绑定 微信绑定
</div> </div>
<div class="info-value wechat-binding"> <div class="info-value wechat-binding">

View File

@@ -1,4 +1,4 @@
export type LayoutType = | 'vertical' | 'blankPage'; export type LayoutType = 'default' | 'vertical' | 'blankPage' | 'blankPage';
// 仿豆包折叠逻辑 // 仿豆包折叠逻辑
export type CollapseType export type CollapseType
@@ -25,7 +25,7 @@ export interface DesignConfigState {
// 是否折叠菜单 // 是否折叠菜单
isCollapse: boolean; isCollapse: boolean;
// 安全区是否被悬停 // 安全区是否被悬停
isSafeAreaHover: boolean; isCollapseConversationList: boolean;
// 跟踪是否首次激活悬停 // 跟踪是否首次激活悬停
hasActivatedHover: boolean; hasActivatedHover: boolean;
} }
@@ -65,15 +65,13 @@ const design: DesignConfigState = {
// 需要自定义路由动画可以把 Main 组件样式代码注释放开从新对话切换到带id的路由时会执行这个动画样式 // 需要自定义路由动画可以把 Main 组件样式代码注释放开从新对话切换到带id的路由时会执行这个动画样式
pageAnimateType: 'zoom-fade', pageAnimateType: 'zoom-fade',
// 布局模式 (纵向vertical | ... | 自己定义) // 布局模式 (纵向vertical | ... | 自己定义)
layout: 'vertical', layout: 'default',
// 折叠类型 // 折叠类型
collapseType: 'followSystem', collapseType: 'followSystem',
// 是否折叠菜单 // 是否折叠对话记录菜单
isCollapse: false, isCollapse: false,
// 安全区是否被悬停 // 是否折叠对话记录菜单
isSafeAreaHover: false, isCollapseConversationList: false,
// 跟踪是否首次激活悬停
hasActivatedHover: false,
}; };
export default design; export default design;

View 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,
};
}

View File

@@ -19,8 +19,9 @@ export function useWindowWidthObserver(
const isAboveThreshold = ref(false); const isAboveThreshold = ref(false);
const thresholdRef = ref(threshold); const thresholdRef = ref(threshold);
let prevIsAbove = false; // 记录上一次状态,避免重复触发 let prevIsAbove = false; // 记录上一次状态,避免重复触发
// 待定 待梳理 1227
// 默认逻辑:修改全局折叠状态 // 默认逻辑:修改全局折叠状态
// eslint-disable-next-line unused-imports/no-unused-vars
const updateCollapseState = (isAbove: boolean) => { const updateCollapseState = (isAbove: boolean) => {
// 判断当前的折叠状态 // 判断当前的折叠状态
switch (designStore.collapseType) { switch (designStore.collapseType) {
@@ -70,7 +71,7 @@ export function useWindowWidthObserver(
onChange(newIsAbove); onChange(newIsAbove);
} }
else { else {
updateCollapseState(newIsAbove); // updateCollapseState(newIsAbove);
} }
} }
}; };

View 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>

View File

@@ -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> <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> </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>

View File

@@ -2,8 +2,6 @@
<script setup lang="ts"> <script setup lang="ts">
import SystemAnnouncementDialog from '@/components/SystemAnnouncementDialog/index.vue'; import SystemAnnouncementDialog from '@/components/SystemAnnouncementDialog/index.vue';
import { useSafeArea } from '@/hooks/useSafeArea'; 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 Header from '@/layouts/components/Header/index.vue';
import Main from '@/layouts/components/Main/index.vue'; import Main from '@/layouts/components/Main/index.vue';
import { useAnnouncementStore, useDesignStore } from '@/stores'; import { useAnnouncementStore, useDesignStore } from '@/stores';
@@ -25,7 +23,7 @@ useSafeArea({
}); });
/** 监听窗口大小变化,折叠侧边栏 */ /** 监听窗口大小变化,折叠侧边栏 */
useWindowWidthObserver(); // useWindowWidthObserver();
// 应用加载时检查是否需要显示公告弹窗 // 应用加载时检查是否需要显示公告弹窗
onMounted(() => { onMounted(() => {
@@ -43,7 +41,7 @@ onMounted(() => {
<Header /> <Header />
</el-header> </el-header>
<el-container class="layout-container-main"> <el-container class="layout-container-main">
<Aside /> <!-- <Aside /> -->
<el-main class="layout-main"> <el-main class="layout-main">
<!-- 路由页面 --> <!-- 路由页面 -->
<Main /> <Main />
@@ -55,29 +53,29 @@ onMounted(() => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.layout-container { //.layout-container {
position: relative; // position: relative;
width: 100%; // width: 100%;
height: 100vh; // height: 100vh;
overflow: hidden; // overflow: hidden;
.layout-header { // .layout-header {
padding: 0; // padding: 0;
} // }
.layout-main { // .layout-main {
height: 100%; // height: 100%;
padding: 0; // padding: 0;
} // }
.layout-container-main { // .layout-container-main {
margin-left: var(--sidebar-left-container-default-width, 0); // margin-left: var(--sidebar-left-container-default-width, 0);
transition: margin-left 0.3s ease; // transition: margin-left 0.3s ease;
} // }
} //}
//
/** 去除菜单右侧边框 */ ///** 去除菜单右侧边框 */
.el-menu { //.el-menu {
border-right: none; // border-right: none;
} //}
.layout-scrollbar { //.layout-scrollbar {
width: 100%; // width: 100%;
} //}
</style> </style>

View File

@@ -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>

View 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>

View File

@@ -30,40 +30,40 @@ const popoverRef = ref();
// 弹出面板内容 // 弹出面板内容
const popoverList = ref([ const popoverList = ref([
{ // {
key: '5', // key: '5',
title: '控制台', // title: '控制台',
icon: 'settings-4-fill', // icon: 'settings-4-fill',
}, // },
{ // {
key: '3', // key: '3',
divider: true, // divider: true,
}, // },
{ // {
key: '7', // key: '7',
title: '公告', // title: '公告',
icon: 'notification-fill', // icon: 'notification-fill',
}, // },
{ // {
key: '8', // key: '8',
title: '模型库', // title: '模型库',
icon: 'apps-fill', // icon: 'apps-fill',
}, // },
{ // {
key: '9', // key: '9',
title: '文档', // title: '文档',
icon: 'book-fill', // icon: 'book-fill',
}, // },
//
{ // {
key: '6', // key: '6',
title: '新手引导', // title: '新手引导',
icon: 'dashboard-fill', // icon: 'dashboard-fill',
}, // },
{ // {
key: '3', // key: '3',
divider: true, // divider: true,
}, // },
{ {
key: '4', key: '4',
title: '退出登录', title: '退出登录',
@@ -130,7 +130,9 @@ function handleClick(item: any) {
ElMessage.warning('暂未开放'); ElMessage.warning('暂未开放');
break; break;
case '5': case '5':
openDialog(); // 打开控制台
popoverRef.value?.hide?.();
router.push('/console');
break; break;
case '6': case '6':
handleStartTutorial(); handleStartTutorial();

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,94 +1,50 @@
<!-- Header 头部 --> <!--
&lt;!&ndash; Header 头部 &ndash;&gt;
<script setup lang="ts"> <script setup lang="ts">
import { onKeyStroke } from '@vueuse/core'; import { useRouter } from 'vue-router';
import { SIDE_BAR_WIDTH } from '@/config/index'; import logo from '@/assets/images/logo.png';
import { useDesignStore, useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session';
import AiTutorialBtn from './components/AiTutorialBtn.vue'; import AiTutorialBtn from './components/AiTutorialBtn.vue';
import AnnouncementBtn from './components/AnnouncementBtn.vue'; import AnnouncementBtn from './components/AnnouncementBtn.vue';
import Avatar from './components/Avatar.vue'; import Avatar from './components/Avatar.vue';
import BuyBtn from './components/BuyBtn.vue'; import BuyBtn from './components/BuyBtn.vue';
import Collapse from './components/Collapse.vue';
import ConsoleBtn from './components/ConsoleBtn.vue'; import ConsoleBtn from './components/ConsoleBtn.vue';
import CreateChat from './components/CreateChat.vue';
import LoginBtn from './components/LoginBtn.vue'; import LoginBtn from './components/LoginBtn.vue';
import ModelLibraryBtn from './components/ModelLibraryBtn.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 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() { function handleOpenConsole() {
avatarRef.value?.openDialog?.(); router.push('/console');
} }
</script> </script>
<template> <template>
<div class="header-container"> <div class="header-container">
<div class="header-box relative z-10 top-0 left-0 right-0"> <div class="header-box">
<div class="absolute left-0 right-0 top-0 bottom-0 flex items-center flex-row"> &lt;!&ndash; 左侧logo和品牌区域 &ndash;&gt;
<div <div class="left-section">
class="overflow-hidden flex h-full items-center flex-row flex-1 w-fit flex-shrink-0 min-w-0" <div class="brand-container">
> <el-image :src="logo" alt="logo" fit="contain" class="logo-img" />
<div class="w-full flex items-center flex-row"> <span class="brand-text">意心AI</span>
<!-- 左边 -->
<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> </div>
<!-- 右边 --> &lt;!&ndash; 右侧功能按钮区域 &ndash;&gt;
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row"> <div class="right-section">
<StartChatBtn />
<AnnouncementBtn /> <AnnouncementBtn />
<ModelLibraryBtn /> <ModelLibraryBtn />
<AiTutorialBtn /> <AiTutorialBtn />
<ConsoleBtn @open-console="handleOpenConsole" /> <ConsoleBtn @open-console="handleOpenConsole" />
<BuyBtn v-show="userStore.userInfo" /> <BuyBtn v-show="userStore.userInfo" />
<Avatar v-show="userStore.userInfo" ref="avatarRef" /> <ThemeBtn />
<LoginBtn v-show="!userStore.userInfo" /> <LoginBtn v-show="!userStore.userInfo" />
</div> <Avatar v-show="userStore.userInfo" />
</div> </div>
</div> </div>
</div> </div>
@@ -98,20 +54,483 @@ function handleOpenConsole() {
.header-container { .header-container {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
flex-direction: column;
width: 100%; width: 100%;
height: fit-content; height: var(&#45;&#45;header-container-default-height, 60px);
.header-box { .header-box {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%; width: 100%;
width: calc( height: 100%;
100% - var(--sidebar-left-container-default-width, 0px) - var( padding: 0 16px;
--sidebar-right-container-default-width, background: var(&#45;&#45;header-bg-color, #ffffff);
0px box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
) }
);
height: var(--header-container-default-heigth); // 左侧品牌区域
margin: 0 var(--sidebar-right-container-default-width, 0) 0 .left-section {
var(--sidebar-left-container-default-width, 0); 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(&#45;&#45;brand-color, #000000);
white-space: nowrap;
letter-spacing: -0.5px;
transition: color 0.2s ease;
&:hover {
//color: var(&#45;&#45;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> </style>

View 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>

View File

@@ -0,0 +1,8 @@
<!-- DesignConfig -->
<script setup lang="ts"></script>
<template>
<div>配置页面</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -0,0 +1,8 @@
<!-- Logo -->
<script setup lang="ts"></script>
<template>
<div>Logo</div>
</template>
<style scoped lang="scss"></style>

View 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>

View File

@@ -1,21 +1,41 @@
<!-- 主布局 --> <!-- 主布局 -->
<script setup lang="ts"> <script setup lang="ts">
import type { LayoutType } from '@/config/design'; import type { LayoutType } from '@/config/design';
import { useRoute } from 'vue-router';
import { useResponsive } from '@/hooks/useResponsive';
import LayoutBlankPage from '@/layouts/LayoutBlankPage/index.vue'; 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 LayoutVertical from '@/layouts/LayoutVertical/index.vue';
import { useDesignStore } from '@/stores'; import { useDesignStore } from '@/stores';
// 这里添加布局类型 // 这里添加布局类型
const LayoutComponent: Record<LayoutType, Component> = { const LayoutComponent: Record<LayoutType | 'mobile', Component> = {
default: LayoutDefault,
vertical: LayoutVertical, vertical: LayoutVertical,
blankPage: LayoutBlankPage, blankPage: LayoutBlankPage,
mobile: LayoutMobile,
}; };
const designStore = useDesignStore(); 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(() => { onMounted(() => {
// 更好的做法是等待所有资源加载 // 更好的做法是等待所有资源加载
window.addEventListener('load', () => { window.addEventListener('load', () => {

View 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>

View 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>

View 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>

View File

@@ -1,31 +1,304 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from 'vue-router'; import { Expand, Fold } from '@element-plus/icons-vue';
import ChatDefaul from '@/pages/chat/layouts/chatDefaul/index.vue'; import { computed, ref } from 'vue';
import ChatWithId from '@/pages/chat/layouts/chatWithId/index.vue'; import { useRoute, useRouter } from 'vue-router';
const route = useRoute(); 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> </script>
<template> <template>
<div class="chat-container"> <div class="console-page" :class="{ 'is-collapsed': isCollapsed }">
<!-- 默认聊天页面 --> <!-- 侧边栏导航 -->
<ChatDefaul v-if="!sessionId" /> <div class="nav-sidebar" :class="{ 'is-collapsed': isCollapsed }">
<!-- 带id的聊天页面 --> <div class="nav-header">
<ChatWithId v-else /> <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> </div>
</template> </template>
<style lang="scss" scoped> <style scoped lang="scss">
.chat-container { .console-page {
position: relative; 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; display: flex;
flex-direction: column; 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; align-items: center;
justify-content: center; justify-content: center;
// width: calc(100% - 32px); width: 32px;
height: 100%; height: 32px;
padding: 0 16px; border-radius: 4px;
overflow-anchor: none; 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> </style>

View File

@@ -7,87 +7,114 @@ import { ElMessage } from 'element-plus';
import { nextTick, ref, watch } from 'vue'; import { nextTick, ref, watch } from 'vue';
import ModelSelect from '@/components/ModelSelect/index.vue'; import ModelSelect from '@/components/ModelSelect/index.vue';
import WelecomeText from '@/components/WelecomeText/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 { useFilesStore } from '@/stores/modules/files';
import { useSessionStore } from '@/stores/modules/session'; import { useSessionStore } from '@/stores/modules/session';
// Store 实例
const userStore = useUserStore(); const userStore = useUserStore();
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
const filesStore = useFilesStore(); const filesStore = useFilesStore();
const guideTourStore = useGuideTourStore();
const senderValue = ref(''); // 响应式数据
const senderRef = ref(); const senderValue = ref(''); // 输入框内容
const senderRef = ref(); // Sender 组件引用
const isSending = ref(false); // 发送状态标志 const isSending = ref(false); // 发送状态标志
// 防抖发送函数 /**
const debouncedSend = useDebounceFn(async () => { * 防抖发送消息函数
*/
const debouncedSend = useDebounceFn(
async () => {
// 1. 验证输入
if (!senderValue.value.trim()) { if (!senderValue.value.trim()) {
ElMessage.warning('消息内容不能为空'); ElMessage.warning('消息内容不能为空');
return; return;
} }
// 2. 检查是否正在发送
if (isSending.value) { if (isSending.value) {
ElMessage.warning('请等待上一条消息发送完成'); ElMessage.warning('请等待上一条消息发送完成');
return; return;
} }
const content = senderValue.value; // 3. 准备发送数据
const content = senderValue.value.trim();
isSending.value = true; isSending.value = true;
try { try {
// 4. 保存到本地存储(可选,用于页面刷新后恢复)
localStorage.setItem('chatContent', content); localStorage.setItem('chatContent', content);
// 5. 创建会话
await sessionStore.createSessionList({ await sessionStore.createSessionList({
userId: userStore.userInfo?.userId as number, userId: userStore.userInfo?.userId as number,
sessionContent: content, sessionContent: content,
sessionTitle: content.slice(0, 10), sessionTitle: content.slice(0, 10),
remark: content.slice(0, 10), remark: content.slice(0, 10),
}); });
senderValue.value = ''; // 清空输入框
// 6. 清空输入框
senderValue.value = '';
} }
catch (error: any) { catch (error: any) {
console.error('发送消息失败:', error); console.error('发送消息失败:', error);
ElMessage.error(error); ElMessage.error(error.message || '发送消息失败');
} }
finally { finally {
// 7. 重置发送状态
isSending.value = false; isSending.value = false;
} }
}, 800, { leading: true, trailing: false }); // 800ms防抖 },
800, // 防抖延迟
{ leading: true, trailing: false }, // 立即执行第一次,忽略后续快速点击
);
// 处理发送事件 /**
* 触发发送消息
*/
function handleSend() { function handleSend() {
debouncedSend(); debouncedSend();
} }
/**
* 删除文件卡片
* @param _item 文件项
* @param index 文件索引
*/
function handleDeleteCard(_item: FilesCardProps, index: number) { function handleDeleteCard(_item: FilesCardProps, index: number) {
filesStore.deleteFileByIndex(index); filesStore.deleteFileByIndex(index);
} }
/**
* 监听文件列表变化,自动展开/收起 Sender 头部
*/
watch( watch(
() => filesStore.filesList.length, () => filesStore.filesList.length,
(val) => { (val) => {
if (val > 0) {
nextTick(() => { nextTick(() => {
if (val > 0) {
senderRef.value?.openHeader(); senderRef.value?.openHeader();
});
} }
else { else {
nextTick(() => {
senderRef.value?.closeHeader(); senderRef.value?.closeHeader();
});
} }
});
}, },
); );
</script> </script>
<template> <template>
<div class="chat-defaul-wrap"> <div class="chat-default">
<div class="chat-default-wrap">
<!-- 欢迎文本 -->
<WelecomeText /> <WelecomeText />
<!-- 消息发送器 -->
<Sender <Sender
ref="senderRef" ref="senderRef"
v-model="senderValue" v-model="senderValue"
class="chat-defaul-sender" class="chat-default-sender"
data-tour="chat-sender" data-tour="chat-sender"
:auto-size="{ :auto-size="{
maxRows: 9, maxRows: 9,
@@ -99,17 +126,19 @@ watch(
:loading="isSending" :loading="isSending"
@submit="handleSend" @submit="handleSend"
> >
<!-- 头部文件附件区域 -->
<template #header> <template #header>
<div class="sender-header p-12px pt-6px pb-0px"> <div class="sender-header">
<Attachments <Attachments
:items="filesStore.filesList" :items="filesStore.filesList"
:hide-upload="true" :hide-upload="true"
@delete-card="handleDeleteCard" @delete-card="handleDeleteCard"
> >
<!-- 左侧滚动按钮 -->
<template #prev-button="{ show, onScrollLeft }"> <template #prev-button="{ show, onScrollLeft }">
<div <div
v-if="show" 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" @click="onScrollLeft"
> >
<el-icon> <el-icon>
@@ -118,10 +147,11 @@ watch(
</div> </div>
</template> </template>
<!-- 右侧滚动按钮 -->
<template #next-button="{ show, onScrollRight }"> <template #next-button="{ show, onScrollRight }">
<div <div
v-if="show" 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" @click="onScrollRight"
> >
<el-icon> <el-icon>
@@ -132,37 +162,104 @@ watch(
</Attachments> </Attachments>
</div> </div>
</template> </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 /> <ModelSelect />
</div> </div>
</template> </template>
<!-- 后缀发送加载动画 -->
<template #suffix> <template #suffix>
<el-icon v-if="isSending" class="is-loading"> <el-icon v-if="isSending" class="loading-icon">
<Loading /> <Loading />
</el-icon> </el-icon>
</template> </template>
</Sender> </Sender>
</div> </div>
</div>
</template> </template>
<style scoped lang="scss"> <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; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
min-height: 450px; min-height: 450px;
.chat-defaul-sender { padding: 20px;
box-sizing: border-box;
.chat-default-sender {
width: 100%; 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; margin-left: 8px;
color: var(--el-color-primary); color: var(--el-color-primary);
animation: rotating 2s linear infinite; animation: rotating 2s linear infinite;
@@ -176,4 +273,21 @@ watch(
transform: rotate(360deg); 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> </style>

View File

@@ -562,14 +562,16 @@ function handleImagePreview(url: string) {
<style scoped lang="scss"> <style scoped lang="scss">
.chat-with-id-container { .chat-with-id-container {
padding: 0 20px;
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%; width: 100%;
max-width: 800px;
height: 100%; height: 100%;
.chat-warp { .chat-warp {
max-width: 1000px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;

View 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>

View 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>

View File

@@ -266,6 +266,7 @@ onMounted(() => {
</div> </div>
</div> </div>
<el-button <el-button
v-if="false"
:icon="HomeFilled" :icon="HomeFilled"
class="home-btn" class="home-btn"
round round
@@ -536,8 +537,8 @@ onMounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.model-library-container { .model-library-container {
min-height: 100vh;
background: linear-gradient(180deg, #f5f7fa 0%, #ffffff 100%); background: linear-gradient(180deg, #f5f7fa 0%, #ffffff 100%);
overflow: auto;
// 顶部横幅 // 顶部横幅
.banner-section { .banner-section {
@@ -570,7 +571,7 @@ onMounted(() => {
} }
.banner-content { .banner-content {
max-width: 1400px; //max-width: 1400px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -686,7 +687,7 @@ onMounted(() => {
padding: 32px 16px; padding: 32px 16px;
.content-wrapper { .content-wrapper {
max-width: 100%; //max-width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 0 8px; padding: 0 8px;
display: flex; display: flex;

View File

@@ -5,19 +5,20 @@ import { ROUTER_WHITE_LIST } from '@/config';
import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter'; import { errorRouter, layoutRouter, staticRouter } from '@/routers/modules/staticRouter';
import { useDesignStore, useUserStore } from '@/stores'; import { useDesignStore, useUserStore } from '@/stores';
// 创建页面加载进度条,提升用户体验。
const { start, done } = useNProgress(0, { const { start, done } = useNProgress(0, {
showSpinner: false, showSpinner: false, // 不显示旋转器
trickleSpeed: 200, trickleSpeed: 200, // 进度条增长速度(毫秒)
minimum: 0.3, minimum: 0.3, // 最小进度值30%
easing: 'ease', easing: 'ease', // 动画缓动函数
speed: 500, speed: 500, // 动画速度
}); });
// 创建路由实例
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(), // 使用 HTML5 History 模式
routes: [...layoutRouter, ...staticRouter, ...errorRouter], routes: [...layoutRouter, ...staticRouter, ...errorRouter], // 合并所有路由
strict: false, strict: false, // 不严格匹配尾部斜杠
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }), // 路由切换时滚动到顶部
}); });
// 路由前置守卫 // 路由前置守卫
@@ -27,14 +28,16 @@ router.beforeEach(
_from: RouteLocationNormalized, _from: RouteLocationNormalized,
next: NavigationGuardNext, next: NavigationGuardNext,
) => { ) => {
// 1. 获取状态管理
const userStore = useUserStore(); const userStore = useUserStore();
const designStore = useDesignStore(); // 必须在守卫内部调用 const designStore = useDesignStore(); // 必须在守卫内部调用
designStore._setLayout(to.meta?.layout || 'vertical'); // 2. 设置布局根据路由meta中的layout配置
designStore._setLayout(to.meta?.layout || 'default');
// 1、NProgress 开始 // 3. 开始显示进度条
start(); start();
// 2、标题 // 4. 设置页面标题
document.title = (to.meta.title as string) || (import.meta.env.VITE_WEB_TITLE as string); document.title = (to.meta.title as string) || (import.meta.env.VITE_WEB_TITLE as string);
// 3、权限 预留 // 3、权限 预留
@@ -52,16 +55,17 @@ router.beforeEach(
// return next(); // return next();
// } // }
// 4、判断访问页面是否在路由白名单地址[静态路由]中,如果存在直接放行。 // 4、判断访问页面是否在路由白名单地址[静态路由]中,如果存在直接放行。
// 5. 白名单检查(跳过权限验证)
if (ROUTER_WHITE_LIST.includes(to.path)) if (ROUTER_WHITE_LIST.includes(to.path))
return next(); return next();
// 5、判断是否有 Token,没有重定向到 login 页面。 // 6. Token 检查(用户认证),没有重定向到 login 页面。
if (!userStore.token) if (!userStore.token)
userStore.logout(); userStore.logout();
// 其余逻辑 预留... // 其余逻辑 预留...
// 6、正常访问页面。 // 8. 放行路由
next(); next();
}, },
); );

View File

@@ -1,38 +1,72 @@
import type { RouteRecordRaw } from 'vue-router'; import type { RouteRecordRaw } from 'vue-router';
import { HOME_URL } from '@/config';
// LayoutRouter[布局路由] // LayoutRouter[布局路由]
export const layoutRouter: RouteRecordRaw[] = [ export const layoutRouter: RouteRecordRaw[] = [
{ {
path: '/', path: '/',
redirect: HOME_URL,
component: () => import('@/layouts/index.vue'), component: () => import('@/layouts/index.vue'),
children: [ children: [
// 将首页重定向逻辑放在这里
{ {
path: HOME_URL, path: '',
redirect: '/chat/conversation',
},
// chat 路由组 - 修正路径和重定向
{
path: 'chat',
name: 'chat', name: 'chat',
component: () => import('@/pages/chat/index.vue'), component: () => import('@/pages/chat/index.vue'),
meta: { meta: {
// title: '通用聊天页面', title: 'AI聊天',
isDefaultChat: true,
icon: 'HomeFilled', icon: 'HomeFilled',
// isHide: '1', // 是否在菜单中隐藏[0是1否] 预留 },
// isKeepAlive: '0', // 是否缓存路由数据[0是1否] 预留 children: [
// isFull: '1', // 是否全屏[0是1否] 预留 // chat 根路径重定向到 conversation
// enName: "Master Station", // 英文名称 预留 {
path: '',
redirect: '/chat/conversation',
},
{
path: 'conversation',
name: 'chatConversation',
component: () => import('@/pages/chat/conversation/index.vue'),
meta: {
title: 'AI对话',
isDefaultChat: true,
}, },
}, },
{ {
path: '/chat/:id', path: 'conversation/:id',
name: 'chatWithId', name: 'chatConversationWithId',
component: () => import('@/pages/chat/index.vue'), component: () => import('@/pages/chat/conversation/index.vue'),
meta: { meta: {
// title: '带 ID 的聊天页面', title: 'AI对话',
isDefaultChat: false, 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', name: 'products',
component: () => import('@/pages/products/index.vue'), component: () => import('@/pages/products/index.vue'),
meta: { meta: {
@@ -41,33 +75,37 @@ export const layoutRouter: RouteRecordRaw[] = [
isDefaultChat: false, isDefaultChat: false,
layout: 'blankPage', layout: 'blankPage',
}, },
}, },
// 模型库
{ {
path: '/model-library', path: 'model-library',
name: 'modelLibrary', name: 'modelLibrary',
component: () => import('@/pages/modelLibrary/index.vue'), component: () => import('@/pages/modelLibrary/index.vue'),
meta: { meta: {
title: '模型库', title: '模型库',
keepAlive: 0, keepAlive: 0,
isDefaultChat: false, isDefaultChat: false,
layout: 'blankPage', layout: 'default',
}, },
}, },
// 支付结果
{ {
path: '/pay-result', path: 'pay-result',
name: 'payResult', name: 'payResult',
component: () => import('@/pages/payResult/index.vue'), component: () => import('@/pages/payResult/index.vue'),
meta: { meta: {
title: '支付结果', title: '支付结果',
keepAlive: 0, // 如果需要缓存 keepAlive: 0,
isDefaultChat: false, // 根据实际情况设置 isDefaultChat: false,
layout: 'blankPage', // 如果需要自定义布局 layout: 'blankPage',
},
}, },
}, // 活动详情
{ {
path: '/activity/:id', path: 'activity/:id',
name: 'activityDetail', name: 'activityDetail',
component: () => import('@/pages/activity/detail.vue'), component: () => import('@/pages/activity/detail.vue'),
meta: { meta: {
@@ -76,8 +114,10 @@ export const layoutRouter: RouteRecordRaw[] = [
layout: 'blankPage', layout: 'blankPage',
}, },
}, },
// 公告详情
{ {
path: '/announcement/:id', path: 'announcement/:id',
name: 'announcementDetail', name: 'announcementDetail',
component: () => import('@/pages/announcement/detail.vue'), component: () => import('@/pages/announcement/detail.vue'),
meta: { meta: {
@@ -86,11 +126,92 @@ export const layoutRouter: RouteRecordRaw[] = [
layout: 'blankPage', 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[静态路由] 预留 // staticRouter[静态路由] 预留
export const staticRouter: RouteRecordRaw[] = []; export const staticRouter: RouteRecordRaw[] = [];

View File

@@ -10,9 +10,9 @@ const {
pageAnimateType: rePageAnimateType, pageAnimateType: rePageAnimateType,
layout: reLayout, layout: reLayout,
collapseType: reCollapseType, collapseType: reCollapseType,
isCollapse: reisCollapse, isCollapse: reIsCollapse,
isSafeAreaHover: reisSafeAreaHover, isCollapseConversationList: reIsCollapseConversationList,
hasActivatedHover: rehasActivatedHover,
} = designSetting; } = designSetting;
export const useDesignStore = defineStore( 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) => { const setCollapse = (collapseFinal: boolean) => {
console.log('c---', collapseFinal);
isCollapse.value = collapseFinal; isCollapse.value = collapseFinal;
}; };
const toggleCollapse = () => {
// 折叠按钮是否被悬停 isCollapse.value = !isCollapse.value;
const isSafeAreaHover = ref<boolean>(reisSafeAreaHover); };
// 新- 对话列表折叠
const setSafeAreaHover = (hover: boolean) => { const isCollapseConversationList = ref<boolean>(reIsCollapseConversationList);
isSafeAreaHover.value = hover; 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 { return {
darkMode, darkMode,
setDarkMode, setDarkMode,
@@ -92,10 +73,11 @@ export const useDesignStore = defineStore(
setCollapseType, setCollapseType,
isCollapse, isCollapse,
setCollapse, setCollapse,
isSafeAreaHover,
setSafeAreaHover,
hasActivatedHover,
_setLayout, _setLayout,
toggleCollapse,
isCollapseConversationList,
setIsCollapseConversationList,
toggleCollapseConversationList,
}; };
}, },
{ {

View File

@@ -36,7 +36,7 @@ export const useSessionStore = defineStore('session', () => {
try { try {
// 清空当前选中会话信息 // 清空当前选中会话信息
setCurrentSession(null); setCurrentSession(null);
router.replace({ name: 'chat' }); router.replace({ name: 'chatConversationWithId' });
} }
catch (error) { catch (error) {
console.error('createSessionBtn错误:', error); console.error('createSessionBtn错误:', error);
@@ -109,7 +109,7 @@ export const useSessionStore = defineStore('session', () => {
const createSessionList = async (data: Omit<CreateSessionDTO, 'id'>) => { const createSessionList = async (data: Omit<CreateSessionDTO, 'id'>) => {
if (!userStore.token) { if (!userStore.token) {
router.replace({ router.replace({
name: 'chatWithId', name: 'chatConversationWithId',
params: { params: {
id: 'not_login', id: 'not_login',
}, },
@@ -140,7 +140,7 @@ export const useSessionStore = defineStore('session', () => {
setCurrentSession(newSessionRes.data); setCurrentSession(newSessionRes.data);
// 跳转聊天页 // 跳转聊天页
router.replace({ router.replace({
name: 'chatWithId', name: 'chatConversationWithId',
params: { id: `${res.data.id}` }, params: { id: `${res.data.id}` },
}); });
} }

View File

@@ -30,7 +30,7 @@ export const useUserStore = defineStore(
// 如果需要调用接口,可以在这里调用 // 如果需要调用接口,可以在这里调用
clearToken(); clearToken();
clearUserInfo(); clearUserInfo();
router.replace({ name: 'chat' }); router.replace({ name: 'chatConversationWithId' });
}; };
// 新增:登录弹框状态 // 新增:登录弹框状态

View File

@@ -5,5 +5,5 @@
@use './elx'; @use './elx';
@use './guide-tour'; @use './guide-tour';
body{ body{
overflow: hidden; //overflow: hidden;
} }

View File

@@ -1,12 +1,69 @@
/* 定义全局变量 */
:root { :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-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-width: 738px;
--login-dialog-height: 416px; --login-dialog-height: 416px;
--login-dialog-padding: 0px; --login-dialog-padding: 0px;
@@ -15,10 +72,179 @@
--login-dialog-mode-toggle-color: #409eff; --login-dialog-mode-toggle-color: #409eff;
--login-dialog-logo-background: #ffffff; --login-dialog-logo-background: #ffffff;
--login-dialog-logo-text-color: #191919; --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-border-radius-base: 12px !important;
--el-messagebox-border-radius: 16px !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;
}
} }

View File

@@ -13,7 +13,8 @@
line-height: 28px !important; line-height: 28px !important;
} }
.chat-with-id-container{ .chat-with-id-container{
max-width: 1000px !important; //待定
//max-width: 1000px !important;
} }
.el-bubble-content-wrapper { .el-bubble-content-wrapper {
--bubble-content-max-width: 800px; --bubble-content-max-width: 800px;

View File

@@ -25,8 +25,6 @@ declare module 'vue' {
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] 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'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider'] ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -49,6 +47,7 @@ declare module 'vue' {
ElSegmented: typeof import('element-plus/es')['ElSegmented'] ElSegmented: typeof import('element-plus/es')['ElSegmented']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']