fix: 前端页面架构重构优化

This commit is contained in:
Gsh
2026-01-02 22:47:09 +08:00
parent ba95d1798f
commit d25ca6dc4a
18 changed files with 227 additions and 541 deletions

View File

@@ -153,7 +153,6 @@ function toggleSidebar() {
<div
v-else
class="header-content-collapsed flex items-center justify-center hover:cursor-pointer"
@click="handleLogoClick"
>
<el-icon size="20">
<ChatLineSquare />
@@ -188,7 +187,7 @@ function toggleSidebar() {
class="creat-chat-btn"
:class="{
'creat-chat-btn-collapsed': isCollapsed,
'is-disabled': isNewChatState
'is-disabled': isNewChatState,
}"
@click="!isNewChatState && handleCreatChat()"
>
@@ -306,7 +305,7 @@ function toggleSidebar() {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
flex-shrink: 0;
background-color: var(--sidebar-background-color, #f9fafb);
//background-color: var(--sidebar-background-color, #f9fafb);
// 展开状态 - 240px
&:not(.aside-collapsed) {

View File

@@ -1,18 +1,8 @@
<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();
}
@@ -20,23 +10,17 @@ function openAnnouncement() {
<template>
<div class="announcement-btn-container" data-tour="announcement-btn">
<el-badge
is-dot
class="announcement-badge"
<div
class="announcement-btn"
title="查看公告"
@click="openAnnouncement"
>
<!-- :value="unreadCount" -->
<!-- :hidden="unreadCount === 0" -->
<!-- :max="99" -->
<div
class="announcement-btn"
title="查看公告"
@click="openAnnouncement"
>
<!-- PC端显示文字 -->
<span class="pc-text">公告</span>
<!-- 移动端显示图标 -->
<!-- PC端显示文字 -->
<span class="pc-text">公告</span>
<!-- 移动端显示图标 -->
<div class="mobile-icon">
<svg
class="mobile-icon"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
@@ -51,62 +35,142 @@ function openAnnouncement() {
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
</div>
</el-badge>
</div>
</div>
</template>
<style scoped lang="scss">
<style scoped>
.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;
position: relative;
padding: 4px 8px;
border-radius: 4px;
}
.announcement-btn:hover {
color: #66b1ff;
transform: translateY(-1px);
background-color: rgba(64, 158, 255, 0.1);
}
/* PC端文字样式 */
.pc-text {
display: inline-block;
position: relative;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
line-height: 1.2;
padding: 2px 8px 2px 0;
}
/* 红点样式 - 缩小到一半 */
.pc-text::after,
.mobile-icon::after {
content: '';
position: absolute;
width: 6px; /* 缩小到6px */
height: 6px; /* 缩小到6px */
background-color: #f56c6c;
border-radius: 50%;
border: 1.5px solid #fff; /* 边框也相应缩小 */
box-shadow: 0 1px 2px rgba(245, 108, 108, 0.3);
animation: pulse 1.8s infinite;
z-index: 1;
}
/* PC端红点位置 - 调整位置使红点正好与"告"字相交 */
.pc-text::after {
top: -4px; /* 向上移动,与文字相交 */
right: -4px; /* 向右移动,与文字相交 */
}
/* 为小红点添加微小的光晕效果 */
.pc-text::before {
content: '';
position: absolute;
top: -6px;
right: -6px;
width: 10px; /* 光晕也相应缩小 */
height: 10px; /* 光晕也相应缩小 */
background-color: rgba(245, 108, 108, 0.2);
border-radius: 50%;
animation: glow 2s infinite;
z-index: 0;
}
@keyframes glow {
0%, 100% {
transform: scale(1);
opacity: 0.3;
}
50% {
transform: scale(1.1);
opacity: 0.4;
}
}
/* 移动端图标样式 */
.mobile-icon {
display: none;
position: relative;
width: 20px;
height: 20px;
}
/* 移动端图标内的红点位置 */
.mobile-icon::after {
top: -2px; /* 位置微调 */
right: -2px; /* 位置微调 */
width: 5px; /* 移动端红点更小一点 */
height: 5px; /* 移动端红点更小一点 */
}
/* 呼吸动画效果 - 调整为更微妙的动画 */
@keyframes pulse {
0% {
transform: scale(0.9);
box-shadow: 0 1px 2px rgba(245, 108, 108, 0.2);
}
50% {
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(245, 108, 108, 0.4);
}
100% {
transform: scale(0.9);
box-shadow: 0 1px 2px rgba(245, 108, 108, 0.2);
}
}
/* 移动端显示图标,隐藏文字 */
@media (max-width: 768px) {
.pc-text {
display: none;
}
.mobile-icon {
display: block;
}
.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;
}
padding: 8px;
}
}
// 移动端显示图标,隐藏文字
@media (max-width: 768px) {
.announcement-btn-container {
.announcement-btn {
.pc-text {
display: none;
}
.mobile-icon {
display: inline;
}
}
/* 响应式调整 */
@media (min-width: 769px) {
.mobile-icon {
display: none;
}
}
</style>

View File

@@ -1,23 +1,19 @@
<!-- 头像 -->
<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 { ref } 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 { 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 { startHeaderTour } = useGuideTour();
/* 弹出面板 开始 */
const popoverStyle = ref({
@@ -29,41 +25,17 @@ 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: '1',
title: '用户信息',
icon: 'settings-4-fill',
},
// 待定
// {
// key: '6',
// title: '新手引导',
// icon: 'dashboard-fill',
// },
// {
// key: '3',
// divider: true,
// },
{
key: '4',
title: '退出登录',
@@ -71,87 +43,20 @@ const popoverList = ref([
},
]);
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');
router.push('/console/user');
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('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
@@ -175,10 +80,7 @@ function handleClick(item: any) {
});
})
.catch(() => {
// ElMessage({
// type: 'info',
// message: '取消',
// });
// 取消退出,不执行任何操作
});
break;
default:
@@ -222,124 +124,15 @@ function openVipGuide() {
});
})
.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="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="user-info-display cursor-pointer flex flex-col text-right mr-2 leading-tight">
<div class="text-sm font-semibold text-gray-800">
{{ userStore.userInfo?.user.nick ?? '未登录用户' }}
</div>
@@ -356,6 +149,7 @@ defineExpose({
<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"
@click="openVipGuide"
>
普通用户
</span>
@@ -363,7 +157,7 @@ defineExpose({
</div>
<!-- 头像区域 -->
<div class="avatar-container" data-tour="user-avatar">
<div class="avatar-container">
<Popover
ref="popoverRef"
placement="bottom-end"
@@ -395,6 +189,7 @@ defineExpose({
<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"
@click="openVipGuide"
>
普通用户
</span>
@@ -405,7 +200,6 @@ defineExpose({
<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)"
>
@@ -414,73 +208,10 @@ defineExpose({
{{ 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>

View File

@@ -1,131 +1,3 @@
<!--
&lt;!&ndash; Header 头部 &ndash;&gt;
<script setup lang="ts">
import { useRouter } from 'vue-router';
import logo from '@/assets/images/logo.png';
import { useUserStore } from '@/stores';
import AiTutorialBtn from './components/AiTutorialBtn.vue';
import AnnouncementBtn from './components/AnnouncementBtn.vue';
import Avatar from './components/Avatar.vue';
import BuyBtn from './components/BuyBtn.vue';
import ConsoleBtn from './components/ConsoleBtn.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';
const router = useRouter();
const userStore = useUserStore();
// 打开控制台
function handleOpenConsole() {
router.push('/console');
}
</script>
<template>
<div class="header-container">
<div class="header-box">
&lt;!&ndash; 左侧logo和品牌区域 &ndash;&gt;
<div class="left-section">
<div class="brand-container">
<el-image :src="logo" alt="logo" fit="contain" class="logo-img" />
<span class="brand-text">意心AI</span>
</div>
</div>
&lt;!&ndash; 右侧功能按钮区域 &ndash;&gt;
<div class="right-section">
<StartChatBtn />
<AnnouncementBtn />
<ModelLibraryBtn />
<AiTutorialBtn />
<ConsoleBtn @open-console="handleOpenConsole" />
<BuyBtn v-show="userStore.userInfo" />
<ThemeBtn />
<LoginBtn v-show="!userStore.userInfo" />
<Avatar v-show="userStore.userInfo" />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.header-container {
display: flex;
flex-shrink: 0;
width: 100%;
height: var(&#45;&#45;header-container-default-height, 60px);
.header-box {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 100%;
padding: 0 16px;
background: var(&#45;&#45;header-bg-color, #ffffff);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
// 左侧品牌区域
.left-section {
display: flex;
align-items: center;
min-width: fit-content;
flex-shrink: 0;
.brand-container {
display: flex;
align-items: center;
gap: 8px;
.logo-img {
width: 36px; // 优化为更合适的大小
height: 36px;
flex-shrink: 0;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.05);
}
}
.brand-text {
font-size: 22px; // 减小字体大小
font-weight: bold;
color: var(&#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';
@@ -161,6 +33,18 @@ function handleSelect(key: string) {
router.push(key);
}
}
// 修改 AI 聊天菜单的点击事件
function handleAIClick(e: MouseEvent) {
e.stopPropagation(); // 阻止事件冒泡
router.push('/chat/conversation');
}
// 修改控制台菜单的点击事件
function handleConsoleClick(e: MouseEvent) {
e.stopPropagation(); // 阻止事件冒泡
router.push('/console/user');
}
</script>
<template>
@@ -186,7 +70,7 @@ function handleSelect(key: string) {
<!-- AI聊天菜单 -->
<el-sub-menu index="chat" class="chat-submenu" popper-class="custom-popover">
<template #title>
<span class="menu-title">AI聊天</span>
<span class="menu-title" @click="handleAIClick">AI应用</span>
</template>
<el-menu-item index="/chat/conversation">
AI对话
@@ -217,7 +101,7 @@ function handleSelect(key: string) {
<!-- 控制台菜单 -->
<el-sub-menu index="console" class="console-submenu" popper-class="custom-popover">
<template #title>
<ConsoleBtn />
<ConsoleBtn @click="handleConsoleClick" />
</template>
<el-menu-item index="/console/user">
用户信息
@@ -271,7 +155,7 @@ function handleSelect(key: string) {
<style scoped lang="scss">
.header-container {
--menu-hover-bg: #f5f5f5;
--menu-hover-bg: var(--color-white);
--menu-active-color: var(--el-color-primary);
--menu-transition: all 0.2s ease;
@@ -289,6 +173,8 @@ function handleSelect(key: string) {
justify-content: space-between;
height: 100%;
border-bottom: none !important;
//background: var(--color-white);
}
// 左侧品牌区域
@@ -344,7 +230,7 @@ function handleSelect(key: string) {
:deep(.el-sub-menu__title) {
height: 100% !important;
border-bottom: none !important;
padding: 0 12px !important;
padding: 0 4px !important;
color: inherit !important;
&:hover {
@@ -401,7 +287,7 @@ function handleSelect(key: string) {
height: 100%;
display: flex;
align-items: center;
padding: 0 12px;
padding: 0 4px;
margin-left: 4px;
}