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

@@ -3,7 +3,6 @@ import type { GoodsItem } from '@/api/pay';
import { CircleCheck, Loading } from '@element-plus/icons-vue'; import { CircleCheck, Loading } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { createOrder, getOrderStatus } from '@/api'; import { createOrder, getOrderStatus } from '@/api';
import { getGoodsList, GoodsCategoryType } from '@/api/pay'; import { getGoodsList, GoodsCategoryType } from '@/api/pay';
import SupportModelList from '@/components/userPersonalCenter/components/SupportModelList.vue'; import SupportModelList from '@/components/userPersonalCenter/components/SupportModelList.vue';
@@ -305,8 +304,6 @@ async function checkPaymentStatus(outTradeNo: string) {
} }
} }
const router = useRouter();
function toggleDetails() { function toggleDetails() {
showDetails.value = !showDetails.value; showDetails.value = !showDetails.value;
} }
@@ -322,7 +319,8 @@ function onClose() {
function goToActivation() { function goToActivation() {
close(); close();
userStore.openUserCenter('activationCode'); // 使用 window.location 进行跳转,避免 router 注入问题
window.location.href = '/console/activation';
} }
</script> </script>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, computed } from 'vue'; import type { DailyTaskItem, DailyTaskStatusOutput } from '@/api/dailyTask/types';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { getTodayTaskStatus, claimTaskReward } from '@/api/dailyTask'; import { onMounted, ref } from 'vue';
import type { DailyTaskStatusOutput, DailyTaskItem } from '@/api/dailyTask/types'; import { claimTaskReward, getTodayTaskStatus } from '@/api/dailyTask';
const taskData = ref<DailyTaskStatusOutput | null>(null); const taskData = ref<DailyTaskStatusOutput | null>(null);
const loading = ref(false); const loading = ref(false);
@@ -17,15 +17,18 @@ async function fetchTaskStatus() {
try { try {
const res = await getTodayTaskStatus(); const res = await getTodayTaskStatus();
taskData.value = res.data; taskData.value = res.data;
} catch (error: any) { }
catch (error: any) {
ElMessage.error(error?.message || '获取任务状态失败'); ElMessage.error(error?.message || '获取任务状态失败');
} finally { }
finally {
loading.value = false; loading.value = false;
} }
} }
async function handleClaim(task: DailyTaskItem) { async function handleClaim(task: DailyTaskItem) {
if (task.status !== 1) return; if (task.status !== 1)
return;
claiming.value[task.level] = true; claiming.value[task.level] = true;
try { try {
@@ -34,9 +37,11 @@ async function handleClaim(task: DailyTaskItem) {
// 刷新任务状态 // 刷新任务状态
await fetchTaskStatus(); await fetchTaskStatus();
} catch (error: any) { }
catch (error: any) {
ElMessage.error(error?.message || '领取奖励失败'); ElMessage.error(error?.message || '领取奖励失败');
} finally { }
finally {
claiming.value[task.level] = false; claiming.value[task.level] = false;
} }
} }
@@ -76,8 +81,10 @@ function getButtonClass(task: DailyTaskItem): string {
// 获取进度条颜色 // 获取进度条颜色
function getProgressColor(task: DailyTaskItem): string { function getProgressColor(task: DailyTaskItem): string {
if (task.status === 2) return '#FFD700'; // 已完成:金色 if (task.status === 2)
if (task.status === 1) return '#67C23A'; // 可领取:绿 return '#FFD700'; // 已完成:金
if (task.status === 1)
return '#67C23A'; // 可领取:绿色
return '#409EFF'; // 进行中:蓝色 return '#409EFF'; // 进行中:蓝色
} }
</script> </script>
@@ -86,15 +93,21 @@ function getProgressColor(task: DailyTaskItem): string {
<div v-loading="loading" class="daily-task-container"> <div v-loading="loading" class="daily-task-container">
<div class="task-header"> <div class="task-header">
<h2>每日任务</h2> <h2>每日任务</h2>
<p class="task-desc">完成每日任务领取额外尊享包 Token 奖励可累加重复</p> <p class="task-desc">
完成每日任务领取额外尊享包 Token 奖励可累加重复
</p>
</div> </div>
<div v-if="taskData" class="task-content"> <div v-if="taskData" class="task-content">
<!-- 今日消耗统计 --> <!-- 今日消耗统计 -->
<div class="consumption-card"> <div class="consumption-card">
<div class="consumption-icon">🔥</div> <div class="consumption-icon">
🔥
</div>
<div class="consumption-info"> <div class="consumption-info">
<div class="consumption-label">今日尊享包消耗</div> <div class="consumption-label">
今日尊享包消耗
</div>
<div class="consumption-value"> <div class="consumption-value">
{{ formatTokenDisplay(taskData.todayConsumedTokens) }} Tokens {{ formatTokenDisplay(taskData.todayConsumedTokens) }} Tokens
</div> </div>
@@ -109,7 +122,7 @@ function getProgressColor(task: DailyTaskItem): string {
class="task-item" class="task-item"
:class="{ :class="{
'task-completed': task.status === 2, 'task-completed': task.status === 2,
'task-claimable': task.status === 1 'task-claimable': task.status === 1,
}" }"
> >
<div class="task-icon"> <div class="task-icon">
@@ -187,7 +200,6 @@ function getProgressColor(task: DailyTaskItem): string {
<style scoped> <style scoped>
.daily-task-container { .daily-task-container {
padding: 20px;
min-height: 400px; min-height: 400px;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;

View File

@@ -83,10 +83,9 @@ onMounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.premium-service { .premium-service {
padding: 24px;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%); //background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%);
// 美化滚动条 // 美化滚动条
&::-webkit-scrollbar { &::-webkit-scrollbar {
@@ -127,7 +126,6 @@ onMounted(() => {
/* 响应式布局 */ /* 响应式布局 */
@media (max-width: 768px) { @media (max-width: 768px) {
.premium-service { .premium-service {
padding: 12px;
} }
.usage-list-wrapper { .usage-list-wrapper {

View File

@@ -599,18 +599,12 @@ onBeforeUnmount(() => {
<style scoped> <style scoped>
.usage-statistics { .usage-statistics {
padding: 20px;
position: relative; position: relative;
background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease; transition: all 0.3s ease;
height: 100%;
overflow-y: auto; overflow-y: auto;
} }
.usage-statistics:hover { .usage-statistics:hover {
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.12);
} }
.usage-statistics.fullscreen-mode { .usage-statistics.fullscreen-mode {
@@ -620,7 +614,6 @@ onBeforeUnmount(() => {
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 2000; z-index: 2000;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 30px; padding: 30px;
overflow-y: auto; overflow-y: auto;
border-radius: 0; border-radius: 0;
@@ -632,7 +625,6 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
margin-bottom: 30px; margin-bottom: 30px;
padding-bottom: 20px; padding-bottom: 20px;
border-bottom: 2px solid #e9ecef;
} }
.header-actions { .header-actions {
@@ -656,7 +648,6 @@ onBeforeUnmount(() => {
.option-label { .option-label {
text-decoration: line-through; text-decoration: line-through;
color: #909399;
} }
} }
} }
@@ -671,7 +662,6 @@ onBeforeUnmount(() => {
} }
&.disabled-icon { &.disabled-icon {
color: #c0c4cc;
} }
} }
@@ -710,14 +700,10 @@ onBeforeUnmount(() => {
.chart-card { .chart-card {
margin-bottom: 30px; margin-bottom: 30px;
border-radius: 16px; border-radius: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
overflow: hidden; overflow: hidden;
background: white;
} }
.chart-card:hover { .chart-card:hover {
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-4px); transform: translateY(-4px);
} }

View File

@@ -303,7 +303,6 @@ function bindWechat() {
<style scoped> <style scoped>
.user-profile { .user-profile {
padding: 20px;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
} }

View File

@@ -22,7 +22,7 @@ import Header from '@/layouts/components/Header/index.vue';
width: 100%; width: 100%;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
background: var(--color-gray-100); background: var(--color-white);
.layout-header { .layout-header {
padding: 0; padding: 0;
border-bottom: var(--header-border) ; border-bottom: var(--header-border) ;

View File

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

View File

@@ -1,18 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useAnnouncementStore } from '@/stores'; import { useAnnouncementStore } from '@/stores';
const announcementStore = useAnnouncementStore(); 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() { function openAnnouncement() {
announcementStore.openDialog(); announcementStore.openDialog();
} }
@@ -20,23 +10,17 @@ function openAnnouncement() {
<template> <template>
<div class="announcement-btn-container" data-tour="announcement-btn"> <div class="announcement-btn-container" data-tour="announcement-btn">
<el-badge <div
is-dot class="announcement-btn"
class="announcement-badge" title="查看公告"
@click="openAnnouncement"
> >
<!-- :value="unreadCount" --> <!-- PC端显示文字 -->
<!-- :hidden="unreadCount === 0" --> <span class="pc-text">公告</span>
<!-- :max="99" -->
<div <!-- 移动端显示图标 -->
class="announcement-btn" <div class="mobile-icon">
title="查看公告"
@click="openAnnouncement"
>
<!-- PC端显示文字 -->
<span class="pc-text">公告</span>
<!-- 移动端显示图标 -->
<svg <svg
class="mobile-icon"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="20" width="20"
height="20" height="20"
@@ -51,62 +35,142 @@ function openAnnouncement() {
<path d="M13.73 21a2 2 0 0 1-3.46 0" /> <path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg> </svg>
</div> </div>
</el-badge> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped>
.announcement-btn-container { .announcement-btn-container {
display: flex; display: flex;
align-items: center; align-items: center;
}
.announcement-badge { .announcement-btn {
:deep(.el-badge__content) { display: flex;
background-color: #f56c6c; align-items: center;
border: none; 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 { .announcement-btn {
display: flex; padding: 8px;
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) { @media (min-width: 769px) {
.announcement-btn-container { .mobile-icon {
.announcement-btn { display: none;
.pc-text {
display: none;
}
.mobile-icon {
display: inline;
}
}
} }
} }
</style> </style>

View File

@@ -1,23 +1,19 @@
<!-- 头像 --> <!-- 头像 -->
<script setup lang="ts"> <script setup lang="ts">
import { ChatLineRound } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { nextTick, onMounted, ref, watch } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import Popover from '@/components/Popover/index.vue'; import Popover from '@/components/Popover/index.vue';
import SvgIcon from '@/components/SvgIcon/index.vue'; import SvgIcon from '@/components/SvgIcon/index.vue';
import { useGuideTour } from '@/hooks/useGuideTour'; import { useGuideTour } from '@/hooks/useGuideTour';
import { useAnnouncementStore, useGuideTourStore, useUserStore } from '@/stores'; import { useUserStore } from '@/stores';
import { useSessionStore } from '@/stores/modules/session'; import { useSessionStore } from '@/stores/modules/session';
import { getUserProfilePicture, isUserVip } from '@/utils/user'; import { getUserProfilePicture, isUserVip } from '@/utils/user';
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const sessionStore = useSessionStore(); const sessionStore = useSessionStore();
const guideTourStore = useGuideTourStore(); const { startHeaderTour } = useGuideTour();
const announcementStore = useAnnouncementStore();
const { startUserCenterTour } = useGuideTour();
/* 弹出面板 开始 */ /* 弹出面板 开始 */
const popoverStyle = ref({ const popoverStyle = ref({
@@ -29,41 +25,17 @@ const popoverRef = ref();
// 弹出面板内容 // 弹出面板内容
const popoverList = ref([ const popoverList = ref([
{
// { key: '1',
// key: '5', title: '用户信息',
// title: '控制台', icon: 'settings-4-fill',
// 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', // key: '6',
// title: '新手引导', // title: '新手引导',
// icon: 'dashboard-fill', // icon: 'dashboard-fill',
// }, // },
// {
// key: '3',
// divider: true,
// },
{ {
key: '4', key: '4',
title: '退出登录', 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() { function handleStartTutorial() {
startHeaderTour(); startHeaderTour();
} }
// 点击 // 点击
function handleClick(item: any) { function handleClick(item: any) {
switch (item.key) { switch (item.key) {
case '1': case '1':
ElMessage.warning('暂未开放'); router.push('/console/user');
break;
case '2':
ElMessage.warning('暂未开放');
break;
case '5':
// 打开控制台
popoverRef.value?.hide?.();
router.push('/console');
break; break;
case '6': case '6':
handleStartTutorial(); handleStartTutorial();
break; 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': case '4':
popoverRef.value?.hide?.(); popoverRef.value?.hide?.();
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', { ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
@@ -175,10 +80,7 @@ function handleClick(item: any) {
}); });
}) })
.catch(() => { .catch(() => {
// ElMessage({ // 取消退出,不执行任何操作
// type: 'info',
// message: '取消',
// });
}); });
break; break;
default: default:
@@ -222,124 +124,15 @@ function openVipGuide() {
}); });
}) })
.catch(() => { .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> </script>
<template> <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"> <div class="text-sm font-semibold text-gray-800">
{{ userStore.userInfo?.user.nick ?? '未登录用户' }} {{ userStore.userInfo?.user.nick ?? '未登录用户' }}
</div> </div>
@@ -356,6 +149,7 @@ defineExpose({
<span <span
v-else 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" 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> </span>
@@ -363,7 +157,7 @@ defineExpose({
</div> </div>
<!-- 头像区域 --> <!-- 头像区域 -->
<div class="avatar-container" data-tour="user-avatar"> <div class="avatar-container">
<Popover <Popover
ref="popoverRef" ref="popoverRef"
placement="bottom-end" placement="bottom-end"
@@ -395,6 +189,7 @@ defineExpose({
<span <span
v-else 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" 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> </span>
@@ -405,7 +200,6 @@ defineExpose({
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full"> <div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
<div <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)]" 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)" @click="handleClick(item)"
> >
@@ -414,73 +208,10 @@ defineExpose({
{{ item.title }} {{ item.title }}
</div> </div>
</div> </div>
<div v-if="item.divider" class="divder h-1px bg-gray-200 my-4px" />
</div> </div>
</div> </div>
</Popover> </Popover>
</div> </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> </div>
</template> </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"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
@@ -161,6 +33,18 @@ function handleSelect(key: string) {
router.push(key); 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> </script>
<template> <template>
@@ -186,7 +70,7 @@ function handleSelect(key: string) {
<!-- AI聊天菜单 --> <!-- AI聊天菜单 -->
<el-sub-menu index="chat" class="chat-submenu" popper-class="custom-popover"> <el-sub-menu index="chat" class="chat-submenu" popper-class="custom-popover">
<template #title> <template #title>
<span class="menu-title">AI聊天</span> <span class="menu-title" @click="handleAIClick">AI应用</span>
</template> </template>
<el-menu-item index="/chat/conversation"> <el-menu-item index="/chat/conversation">
AI对话 AI对话
@@ -217,7 +101,7 @@ function handleSelect(key: string) {
<!-- 控制台菜单 --> <!-- 控制台菜单 -->
<el-sub-menu index="console" class="console-submenu" popper-class="custom-popover"> <el-sub-menu index="console" class="console-submenu" popper-class="custom-popover">
<template #title> <template #title>
<ConsoleBtn /> <ConsoleBtn @click="handleConsoleClick" />
</template> </template>
<el-menu-item index="/console/user"> <el-menu-item index="/console/user">
用户信息 用户信息
@@ -271,7 +155,7 @@ function handleSelect(key: string) {
<style scoped lang="scss"> <style scoped lang="scss">
.header-container { .header-container {
--menu-hover-bg: #f5f5f5; --menu-hover-bg: var(--color-white);
--menu-active-color: var(--el-color-primary); --menu-active-color: var(--el-color-primary);
--menu-transition: all 0.2s ease; --menu-transition: all 0.2s ease;
@@ -289,6 +173,8 @@ function handleSelect(key: string) {
justify-content: space-between; justify-content: space-between;
height: 100%; height: 100%;
border-bottom: none !important; border-bottom: none !important;
//background: var(--color-white);
} }
// 左侧品牌区域 // 左侧品牌区域
@@ -344,7 +230,7 @@ function handleSelect(key: string) {
:deep(.el-sub-menu__title) { :deep(.el-sub-menu__title) {
height: 100% !important; height: 100% !important;
border-bottom: none !important; border-bottom: none !important;
padding: 0 12px !important; padding: 0 4px !important;
color: inherit !important; color: inherit !important;
&:hover { &:hover {
@@ -401,7 +287,7 @@ function handleSelect(key: string) {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 12px; padding: 0 4px;
margin-left: 4px; margin-left: 4px;
} }

View File

@@ -7,7 +7,7 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
// 控制侧边栏折叠状态 // 控制侧边栏折叠状态
const isCollapsed = ref(false); const isCollapsed = ref(true);
// 菜单项配置 // 菜单项配置
const navItems = [ const navItems = [
@@ -47,7 +47,7 @@ window.addEventListener('resize', checkIsMobile);
<div class="console-page" :class="{ 'is-collapsed': isCollapsed }"> <div class="console-page" :class="{ 'is-collapsed': isCollapsed }">
<!-- 侧边栏导航 --> <!-- 侧边栏导航 -->
<div class="nav-sidebar" :class="{ 'is-collapsed': isCollapsed }"> <div class="nav-sidebar" :class="{ 'is-collapsed': isCollapsed }">
<div class="nav-header"> <div v-if="false" class="nav-header">
<h2 v-show="!isCollapsed" class="nav-title"> <h2 v-show="!isCollapsed" class="nav-title">
AI聊天 AI聊天
</h2> </h2>

View File

@@ -208,7 +208,7 @@ watch(
.chat-header { .chat-header {
width: 100%; width: 100%;
max-width: 1000px; //max-width: 1000px;
height: 60px; height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -607,7 +607,7 @@ function handleImagePreview(url: string) {
.chat-header { .chat-header {
width: 100%; width: 100%;
max-width: 1000px; //max-width: 1000px;
height: 60px; height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -97,7 +97,7 @@ window.addEventListener('resize', checkIsMobile);
<div class="content-main"> <div class="content-main">
<div v-if="isMobile" class="content-header"> <div v-if="isMobile" class="content-header">
<div class="mobile-toggle" @click="isCollapsed = false"> <div class="mobile-toggle" @click="isCollapsed = false">
<el-icon><i-ep-expand /></el-icon> <el-icon><Expand /></el-icon>
<span>菜单</span> <span>菜单</span>
</div> </div>
</div> </div>

View File

@@ -18,7 +18,7 @@ export const layoutRouter: RouteRecordRaw[] = [
name: 'chat', name: 'chat',
component: () => import('@/pages/chat/index.vue'), component: () => import('@/pages/chat/index.vue'),
meta: { meta: {
title: 'AI聊天', title: 'AI应用',
icon: 'HomeFilled', icon: 'HomeFilled',
}, },
children: [ children: [

View File

@@ -199,10 +199,10 @@
/* Element Plus 组件特定变量 */ /* Element Plus 组件特定变量 */
--el-menu-item-height: 48px; --el-menu-item-height: 48px;
--el-menu-bg-color: var(--sidebar-background-color); //--el-menu-bg-color: var(--sidebar-background-color);
--el-menu-text-color: var(--text-color-secondary); //--el-menu-text-color: var(--text-color-secondary);
--el-menu-active-color: var(--color-primary); //--el-menu-active-color: var(--color-primary);
--el-menu-hover-bg-color: var(--color-gray-100); //--el-menu-hover-bg-color: var(--color-gray-100);
/* 表单相关 */ /* 表单相关 */
--el-form-label-font-size: var(--font-size-sm); --el-form-label-font-size: var(--font-size-sm);

View File

@@ -1,5 +1,9 @@
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import ElementPlus from 'element-plus';
import { createApp, h } from 'vue'; import { createApp, h } from 'vue';
import ProductPackage from '@/components/ProductPackage/index.vue'; import ProductPackage from '@/components/ProductPackage/index.vue';
import router from '@/routers';
import store from '@/stores';
export function showProductPackage() { export function showProductPackage() {
const div = document.createElement('div'); const div = document.createElement('div');
@@ -16,5 +20,16 @@ export function showProductPackage() {
}, },
}); });
// 关键:必须在 mount 之前按顺序注册所有依赖
app.use(store); // 1. 先注册 store
app.use(router); // 2. 再注册 router
app.use(ElementPlus); // 3. 最后注册 ElementPlus
// 注册 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
// 最后才挂载应用
app.mount(div); app.mount(div);
} }

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']