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

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