Files
offerpai_web/src/components/SideNav.vue
T
2026-07-10 11:38:56 +08:00

824 lines
43 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="side-nav">
<!-- 顶部 Logo -->
<div class="side-nav__header">
<!-- 侧边栏Logo图片 -->
<img src="@/assets/images/logo.png" alt="Offer派" class="side-nav__logo-img" />
</div>
<!-- 主导航 -->
<nav class="side-nav__menu">
<a
v-for="(item, index) in mainMenus"
:key="item.name"
href="javascript:void(0)"
class="side-nav__item"
:class="{ 'side-nav__item--active': isActive(item.name) }"
@click="handleNav(item)"
@mouseenter="hoverIndex = index"
@mouseleave="hoverIndex = -1"
>
<!-- SVG图标hover/active 状态显示选中图标否则显示默认图标 -->
<span
class="side-nav__item-icon"
v-html="(isActive(item.name) || hoverIndex === index) && getActiveIcon(index) ? getActiveIcon(index) : item.iconImg"
></span>
<span class="side-nav__item-label">{{ item.label }}</span>
<!-- 索引0的时候显示"全网"小标签 -->
<span v-if="index === 0" class="side-nav__tag-quanwang mt2 ml8">全网</span>
<span v-if="item.badge" class="side-nav__badge">{{ item.badge }}</span>
</a>
</nav>
<!-- 底部菜单 -->
<div class="side-nav__footer">
<!-- 邀请会员卡片 -->
<div class="side-nav__referral-card cursor-po" @click="showShareDialog = true">
<!-- 顶部标题区域 -->
<div class="side-nav__referral-title">邀请好友送 7 天会员</div>
<div class="side-nav__referral-subtitle">好友加入后自动到账</div>
<!-- 白色面板 -->
<div class="side-nav__referral-panel">
<!-- 邀请进度行 -->
<div class="side-nav__referral-row">
<span class="side-nav__referral-label">邀请进度</span>
<span class="side-nav__referral-value">{{ milestoneProgress }}/{{ milestoneTarget }}</span>
</div>
<!-- 进度条 -->
<div class="side-nav__referral-track">
<div class="side-nav__referral-fill" :style="{ width: inviteProgressPercent + '%' }"></div>
</div>
<!-- 提示文字 -->
<div class="side-nav__referral-tip">再邀请 <strong>{{ inviteRemaining }}</strong> 可获得年会员</div>
<!-- 分隔线 -->
<div class="side-nav__referral-divider"></div>
<!-- 去邀请按钮 -->
<div class="side-nav__referral-action">去邀请</div>
</div>
</div>
<!-- 用户信息模块 -->
<div class="side-nav__user-info-wrapper" v-if="store.state.isAuthenticated && store.state.userInfo">
<!-- 账户弹窗 -->
<div v-if="showAccountPopup" class="side-nav__account-popup">
<!-- 顶部渐变区域会员信息根据会员类型显示不同渐变背景 -->
<div class="side-nav__account-popup-header" :class="accountHeaderBgClass">
<div class="side-nav__account-popup-header-row">
<div class="side-nav__account-popup-header-left">
<div class="side-nav__account-popup-member-type" :class="accountMemberColorClass">{{ memberTagText }}</div>
<div v-if="memberExpireText" class="side-nav__account-popup-expire">{{ memberExpireText }}</div>
</div>
<div class="side-nav__account-popup-detail" @click="handleAccountDetail">{{ accountDetailBtnText }}</div>
</div>
</div>
<!-- 设置按钮 -->
<div class="side-nav__account-popup-item" @click="handleAccountSettings">设置</div>
<!-- 退出登录按钮 -->
<div class="side-nav__account-popup-item" @click="handleAccountLogout">退出登录</div>
</div>
<!-- 用户信息卡片点击弹出/关闭账户弹窗 -->
<div class="side-nav__user-info" :class="{ 'side-nav__user-info--active': showAccountPopup }" @click.stop="showAccountPopup = !showAccountPopup">
<div class="side-nav__user-left">
<!-- 手机号脱敏显示 -->
<div class="side-nav__user-phone">{{ maskedPhone }}</div>
<!-- 会员标签 -->
<div class="side-nav__user-member-tag" :class="memberTagClass">{{ memberTagText }}</div>
</div>
<!-- 右侧铃铛图标 -->
<div class="side-nav__user-bell" @click.stop="showMessageDialog = true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_422_1692)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.83126 9.77736L6.60626 5.90736C6.76471 5.1241 7.18922 4.41976 7.80778 3.9138C8.42635 3.40785 9.20088 3.13143 10 3.13143C10.7991 3.13143 11.5737 3.40785 12.1922 3.9138C12.8108 4.41976 13.2353 5.1241 13.3938 5.90736L14.1688 9.77736C14.4013 10.9321 14.9687 11.9929 15.8 12.8274L16.0425 13.0686C16.1318 13.1581 16.1956 13.2697 16.2272 13.3921C16.2589 13.5145 16.2572 13.6431 16.2224 13.7646C16.1876 13.8862 16.1209 13.9962 16.0293 14.0832C15.9376 14.1703 15.8244 14.2313 15.7013 14.2599L14.675 14.4974C11.5989 15.2087 8.40109 15.2087 5.32501 14.4974L4.30001 14.2599C4.17678 14.2315 4.06339 14.1706 3.97161 14.0836C3.87983 13.9966 3.81301 13.8866 3.77806 13.7651C3.74312 13.6435 3.74133 13.5149 3.77288 13.3924C3.80444 13.2699 3.86818 13.1581 3.95751 13.0686L4.20001 12.8274C5.03317 11.9942 5.60081 10.9329 5.83126 9.77736ZM3.99376 9.40987L4.76751 5.53987C5.01082 4.33139 5.66493 3.24439 6.61874 2.46347C7.57256 1.68255 8.76728 1.25586 10 1.25586C11.2327 1.25586 12.4275 1.68255 13.3813 2.46347C14.3351 3.24439 14.9892 4.33139 15.2325 5.53987L16.0075 9.40987C16.165 10.2024 16.555 10.9299 17.1263 11.5011L17.3688 11.7436C17.6951 12.0696 17.9281 12.477 18.0437 12.9236C18.1592 13.3701 18.1531 13.8394 18.0259 14.2828C17.8987 14.7262 17.6551 15.1274 17.3203 15.4447C16.9856 15.7621 16.572 15.984 16.1225 16.0874L15.0963 16.3249C14.2188 16.5249 13.3354 16.674 12.4463 16.7724C12.3274 17.3329 12.0196 17.8356 11.5744 18.1963C11.1292 18.557 10.5736 18.7538 10.0006 18.7538C9.42765 18.7538 8.87205 18.557 8.42685 18.1963C7.98165 17.8356 7.67386 17.3329 7.55501 16.7724C6.66352 16.6757 5.77861 16.5259 4.90501 16.3236L3.87751 16.0861C3.4285 15.9822 3.01545 15.7601 2.68118 15.4428C2.34692 15.1255 2.10361 14.7246 1.97647 14.2816C1.84934 13.8386 1.84301 13.3697 1.95814 12.9234C2.07327 12.4772 2.30568 12.0698 2.63126 11.7436L2.87501 11.4999C3.44626 10.9286 3.83626 10.1999 3.99376 9.40862" fill="#6A7282"/>
</g>
<defs>
<clipPath id="clip0_422_1692">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>
<!-- 未读消息红点 -->
<span v-if="unreadCount > 0" class="side-nav__user-bell-dot"></span>
</div>
</div>
</div>
</div>
<!-- 消息通知弹窗 -->
<Teleport to="body">
<div v-if="showMessageDialog" class="side-nav__dialog-overlay" @click="showMessageDialog = false">
<div class="side-nav__message-dialog" @click.stop>
<!-- 弹窗头部 -->
<div class="side-nav__message-dialog-header">
<span>消息通知</span>
<span class="side-nav__dialog-close" @click="showMessageDialog = false">
<!-- 关闭图标 SVG -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6l12 12" stroke="#6A7282" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
</div>
<!-- 弹窗内容区左列表 + 右详情 -->
<div class="side-nav__message-dialog-content">
<!-- 左侧消息列表 -->
<div class="side-nav__message-list" @scroll="handleMessageListScroll">
<div
v-for="(msg, idx) in messageList"
:key="msg.id"
class="side-nav__message-list-item"
:class="{ 'side-nav__message-list-item--active': selectedMessageIdx === idx }"
@click="selectedMessageIdx = idx"
>
<!-- 列表项内容未读圆点 + 标题 + 日期 -->
<div class="side-nav__message-list-item-row">
<div class="side-nav__message-list-item-left">
<span v-if="!msg.read" class="side-nav__message-unread-dot"></span>
<span class="side-nav__message-list-title">{{ msg.title }}</span>
</div>
<span class="side-nav__message-list-date">{{ formatMonthDay(msg.createTime) }}</span>
</div>
</div>
<!-- 加载中提示 -->
<div v-if="messageLoading" class="side-nav__message-list-loading">加载中...</div>
<!-- 无消息提示 -->
<div v-if="!messageLoading && messageList.length === 0" class="side-nav__message-list-empty">暂无消息</div>
</div>
<!-- 右侧消息详情 -->
<div class="side-nav__message-detail">
<template v-if="currentMessage">
<!-- 详情头部标题 + 时间 -->
<div class="side-nav__message-detail-header">
<div class="side-nav__message-detail-title">{{ currentMessage.title }}</div>
<div class="side-nav__message-detail-time">{{ timestampToLocalDateTime(currentMessage.createTime, 'returnSecond') }}</div>
</div>
<!-- 分隔线 -->
<div class="side-nav__message-detail-divider"></div>
<!-- 消息正文 -->
<div class="side-nav__message-detail-content">{{ currentMessage.content }}</div>
</template>
<div v-else class="side-nav__message-detail-empty">请选择一条消息查看</div>
</div>
</div>
</div>
</div>
</Teleport>
<!-- 反馈弹窗 -->
<Teleport to="body">
<div v-if="showFeedbackDialog" class="side-nav__dialog-overlay" @click="showFeedbackDialog = false">
<div class="side-nav__dialog feedback-dialog" @click.stop>
<div class="feedback-dialog__title">反馈与帮助</div>
<div class="feedback-dialog__section">
<div class="feedback-dialog__label">*你想向我们反馈什么问题</div>
<div class="feedback-dialog__options">
<div
v-for="(opt,index) in feedbackOptions"
:key="index"
class="feedback-dialog__option"
:class="{ 'feedback-dialog__option--active': feedbackType === opt }"
@click="feedbackType = opt, feedbackTypeIndex = index+1"
>
{{ opt }}
</div>
</div>
</div>
<div class="feedback-dialog__section">
<div class="feedback-dialog__label">*能否提供更多细节</div>
<textarea
v-model="feedbackDetail"
class="feedback-dialog__textarea"
placeholder="请描述你的使用体验或分享您的想法。描述越具体,我们越能更好地处理您的反馈。"
rows="5"
></textarea>
</div>
<div class="feedback-dialog__actions">
<button class="feedback-dialog__btn feedback-dialog__btn--cancel" @click="showFeedbackDialog = false">取消</button>
<button class="feedback-dialog__btn feedback-dialog__btn--submit" @click="handleFeedbackSubmit">确认提交</button>
</div>
</div>
</div>
</Teleport>
<!-- 设置弹窗 -->
<SettingsDialog v-model="showSettingsDialog" :initial-tab="store.state.settingsTab" />
<!-- 退出登录确认弹窗 -->
<el-dialog v-model="showLogoutConfirm" title="退出登录" width="3.6rem" style="line-height: 0.2rem" :close-on-click-modal="true" :append-to-body="true">
<p style="font-size: 0.14rem; color: #555; text-align: center;">确定要退出当前账号吗</p>
<template #footer>
<el-button @click="showLogoutConfirm = false">取消</el-button>
<el-button type="danger" @click="confirmLogout">确认退出</el-button>
</template>
</el-dialog>
<!-- 邀请注册送会员弹窗 -->
<SettingsInviteDialog v-model="showShareDialog" />
<!-- 会员权限拦截弹窗 -->
<MemberAccessDialog v-model="showMemberAccessDialog" @open-member="showMemberDialog = true" />
<!-- 会员购买弹窗 -->
<MemberDialog v-model="showMemberDialog" />
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStore } from 'vuex'
import SettingsDialog from '@/components/SettingsDialog.vue'
import MemberAccessDialog from '@/components/MemberAccessDialog.vue'
import MemberDialog from '@/components/MemberDialog.vue'
import { checkLogin } from '@/api/auth'
import { logout } from '@/api/auth'
import { fetchMessageList, fetchUnreadCount, markMessageRead } from '@/api/message'
import { fetchInviteStats } from '@/api/member'
import { timestampToLocalDateTime } from '@/utils/time'
/**
* 格式化时间戳为月/日显示(如 06/16)
* 利用 timestampToLocalDateTime 的 returnDay 精度,截取掉年份部分
*/
function formatMonthDay(timestamp: number | null | undefined): string {
const full = timestampToLocalDateTime(timestamp, 'returnDay') // 返回 "2026/06/16"
// 去掉年份的四位数字和第一个斜杠 → "06/16"
return full.replace(/^\d{4}\//, '')
}
import {userFeedback} from '@/api/setting'
import type { MessageDto } from '@/api/message'
import navJobsIcon from '@/assets/images/nav/nav-jobs-icon.png'
import navResumeIcon from '@/assets/images/nav/nav-resume-icon.png'
import navProfileIcon from '@/assets/images/nav/nav-profile-icon.png'
import navAgentIcon from '@/assets/images/nav/nav-agent-icon.png'
import navMessageIcon from '@/assets/images/nav/nav-message-icon.png'
import navSettingIcon from '@/assets/images/nav/nav-setting-icon.png'
import navFeedbackIcon from '@/assets/images/nav/nav-feedback-icon.png'
import navShareIcon from '@/assets/images/nav/nav-share-icon.png'
// 导师页图标(暂用 profile 图标占位,有设计稿后替换为专属图标)
import navMentorIcon from '@/assets/images/nav/nav-profile-icon.png'
/**
* 选中/hover 状态下的图标 SVG 数组
* 按索引与 mainMenus 中的菜单项一一对应
* 索引0 对应第一个菜单项,索引1 对应第二个,以此类推
*/
const activeIconSvgs: string[] = [
// 索引0 — 职位(Jobs
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1536)"><path d="M7.66664 2.16669C6.78388 2.16674 5.91411 2.37928 5.13081 2.78634C4.34751 3.1934 3.6737 3.78302 3.16631 4.50539C2.65891 5.22775 2.33285 6.06164 2.21567 6.93658C2.09849 7.81152 2.19362 8.70182 2.49305 9.53224C2.79247 10.3627 3.28738 11.1088 3.93596 11.7077C4.58454 12.3065 5.36773 12.7404 6.21936 12.9728C7.07099 13.2051 7.96603 13.229 8.82886 13.0426C9.6917 12.8561 10.497 12.4647 11.1766 11.9014L12.962 13.6867C13.0568 13.775 13.1821 13.8231 13.3117 13.8208C13.4412 13.8185 13.5648 13.766 13.6564 13.6744C13.748 13.5828 13.8005 13.4592 13.8028 13.3297C13.805 13.2002 13.757 13.0748 13.6686 12.98L11.8853 11.196C12.5567 10.3935 12.9853 9.41618 13.1208 8.37866C13.2562 7.34115 13.0929 6.28652 12.65 5.33856C12.2071 4.3906 11.503 3.58865 10.6203 3.02682C9.73759 2.465 8.71296 2.16661 7.66664 2.16669Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1536"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
// 索引1 — 第二个菜单项(待替换)
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1540)"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.49998 1.66667C9.49998 1.62246 9.48242 1.58007 9.45116 1.54882C9.41991 1.51756 9.37752 1.5 9.33331 1.5H4.66665C4.18042 1.5 3.7141 1.69315 3.37028 2.03697C3.02647 2.38079 2.83331 2.8471 2.83331 3.33333V12.6667C2.83331 13.1529 3.02647 13.6192 3.37028 13.963C3.7141 14.3068 4.18042 14.5 4.66665 14.5H11.3333C11.8195 14.5 12.2859 14.3068 12.6297 13.963C12.9735 13.6192 13.1666 13.1529 13.1666 12.6667V6.098C13.1666 6.0538 13.1491 6.0114 13.1178 5.98015C13.0866 5.94889 13.0442 5.93133 13 5.93133H9.99998C9.86737 5.93133 9.74019 5.87865 9.64643 5.78489C9.55266 5.69112 9.49998 5.56394 9.49998 5.43133V1.66667ZM9.99998 8.16667C10.1326 8.16667 10.2598 8.21935 10.3535 8.31311C10.4473 8.40688 10.5 8.53406 10.5 8.66667C10.5 8.79927 10.4473 8.92645 10.3535 9.02022C10.2598 9.11399 10.1326 9.16667 9.99998 9.16667H5.99998C5.86737 9.16667 5.74019 9.11399 5.64643 9.02022C5.55266 8.92645 5.49998 8.79927 5.49998 8.66667C5.49998 8.53406 5.55266 8.40688 5.64643 8.31311C5.74019 8.21935 5.86737 8.16667 5.99998 8.16667H9.99998ZM9.99998 10.8333C10.1326 10.8333 10.2598 10.886 10.3535 10.9798C10.4473 11.0735 10.5 11.2007 10.5 11.3333C10.5 11.4659 10.4473 11.5931 10.3535 11.6869C10.2598 11.7807 10.1326 11.8333 9.99998 11.8333H5.99998C5.86737 11.8333 5.74019 11.7807 5.64643 11.6869C5.55266 11.5931 5.49998 11.4659 5.49998 11.3333C5.49998 11.2007 5.55266 11.0735 5.64643 10.9798C5.74019 10.886 5.86737 10.8333 5.99998 10.8333H9.99998Z" fill="#52CAD1"/><path d="M10.5 1.88266C10.5 1.75999 10.6287 1.68199 10.724 1.75866C10.8049 1.82399 10.8767 1.89999 10.9393 1.98666L12.948 4.78466C12.9933 4.84866 12.944 4.93132 12.8653 4.93132H10.6667C10.6225 4.93132 10.5801 4.91377 10.5488 4.88251C10.5176 4.85125 10.5 4.80886 10.5 4.76466V1.88266Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1540"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
// 索引2 — 第三个菜单项(待替换)
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1537)"><path d="M13.8086 5.41868L13.6446 5.79601C13.619 5.85745 13.5758 5.90994 13.5204 5.94686C13.465 5.98378 13.3999 6.00348 13.3333 6.00348C13.2667 6.00348 13.2017 5.98378 13.1462 5.94686C13.0908 5.90994 13.0476 5.85745 13.022 5.79601L12.858 5.41868C12.5696 4.75116 12.0416 4.21596 11.378 3.91868L10.872 3.69268C10.8106 3.66444 10.7586 3.61919 10.7221 3.56229C10.6857 3.5054 10.6663 3.43925 10.6663 3.37168C10.6663 3.3041 10.6857 3.23795 10.7221 3.18106C10.7586 3.12416 10.8106 3.07891 10.872 3.05068L11.35 2.83801C12.0302 2.53226 12.5674 1.97726 12.8506 1.28734L13.0193 0.880009C13.0441 0.816911 13.0873 0.762741 13.1433 0.724559C13.1993 0.686377 13.2655 0.665955 13.3333 0.665955C13.4011 0.665955 13.4673 0.686377 13.5233 0.724559C13.5793 0.762741 13.6225 0.816911 13.6473 0.880009L13.816 1.28668C14.099 1.97673 14.6358 2.53196 15.316 2.83801L15.7946 3.05134C15.8558 3.07966 15.9077 3.1249 15.944 3.18172C15.9803 3.23855 15.9996 3.30457 15.9996 3.37201C15.9996 3.43944 15.9803 3.50547 15.944 3.56229C15.9077 3.61911 15.8558 3.66436 15.7946 3.69268L15.288 3.91801C14.6245 4.21559 14.0967 4.75103 13.8086 5.41868ZM13.3333 7.33334C13.7858 7.3337 14.2351 7.25728 14.662 7.10734C14.6651 7.1829 14.6666 7.25823 14.6666 7.33334C14.6666 8.74783 14.1047 10.1044 13.1045 11.1046C12.1044 12.1048 10.7478 12.6667 9.33331 12.6667V15C5.99998 13.6667 1.33331 11.6667 1.33331 7.33334C1.33331 5.91885 1.89522 4.5623 2.89541 3.56211C3.8956 2.56191 5.25216 2.00001 6.66665 2.00001H9.33331C9.40887 2.00001 9.4842 2.00156 9.55931 2.00468C9.34686 2.60796 9.28217 3.25339 9.37068 3.88684C9.45918 4.52028 9.69829 5.12327 10.068 5.64522C10.4376 6.16717 10.9271 6.59286 11.4952 6.88659C12.0634 7.18032 12.6937 7.33352 13.3333 7.33334Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1537"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
// 索引3 — 第四个菜单项(待替换)
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1539)"><path d="M13.88 2.97668C13.253 2.36774 12.4141 2.02596 11.54 2.02334H4.50002C4.06562 2.01717 3.6343 2.09718 3.23102 2.25875C2.82773 2.42032 2.46048 2.66025 2.1505 2.96464C1.84053 3.26904 1.59398 3.63187 1.42511 4.03216C1.25624 4.43244 1.16841 4.86223 1.16669 5.29668V10.7033C1.1684 11.1341 1.25528 11.5602 1.42233 11.9572C1.58937 12.3542 1.83328 12.7143 2.14002 13.0167C2.76361 13.6317 3.60419 13.9765 4.48002 13.9767H11.52C12.3959 13.9765 13.2364 13.6317 13.86 13.0167C14.1689 12.7159 14.4142 12.3561 14.5813 11.9587C14.7485 11.5614 14.8342 11.1345 14.8334 10.7033V5.29668C14.8362 4.86607 14.7534 4.43919 14.5898 4.0409C14.4261 3.6426 14.1848 3.28084 13.88 2.97668ZM7.77335 11.1433C7.64075 11.1433 7.51357 11.0907 7.4198 10.9969C7.32603 10.9031 7.27335 10.776 7.27335 10.6433C7.27312 10.4413 7.23295 10.2412 7.15515 10.0548C7.07735 9.86827 6.96346 9.699 6.82002 9.55667C6.52064 9.27977 6.12783 9.12596 5.72002 9.12596C5.31221 9.12596 4.9194 9.27977 4.62002 9.55667C4.47599 9.69855 4.3617 9.86774 4.28386 10.0543C4.20602 10.2409 4.16618 10.4412 4.16669 10.6433C4.16669 10.776 4.11401 10.9031 4.02024 10.9969C3.92647 11.0907 3.7993 11.1433 3.66669 11.1433C3.53408 11.1433 3.4069 11.0907 3.31313 10.9969C3.21937 10.9031 3.16669 10.776 3.16669 10.6433C3.16616 10.3089 3.23187 9.97762 3.36002 9.66868C3.48817 9.35973 3.67623 9.07923 3.91335 8.84334C4.14167 8.60543 4.41317 8.41313 4.71335 8.27668C4.44349 8.11088 4.22081 7.87847 4.0667 7.60178C3.91259 7.32508 3.83223 7.01339 3.83335 6.69668C3.83335 6.2016 4.03002 5.72681 4.38009 5.37674C4.73016 5.02667 5.20495 4.83001 5.70002 4.83001C6.19509 4.83001 6.66989 5.02667 7.01995 5.37674C7.37002 5.72681 7.56669 6.2016 7.56669 6.69668C7.57063 7.01088 7.49433 7.3209 7.345 7.59738C7.19568 7.87387 6.97827 8.10767 6.71335 8.27668C7.01802 8.40334 7.29669 8.58668 7.53335 8.81668C8.00738 9.29639 8.2753 9.94228 8.28002 10.6167C8.2847 10.6853 8.27496 10.7542 8.25142 10.8189C8.22789 10.8836 8.19107 10.9426 8.14335 10.9922C8.09563 11.0418 8.03806 11.0809 7.97434 11.1069C7.91062 11.133 7.84215 11.1454 7.77335 11.1433ZM12.1267 10.5567H9.73335C9.60075 10.5567 9.47357 10.504 9.3798 10.4102C9.28603 10.3165 9.23335 10.1893 9.23335 10.0567C9.23335 9.92407 9.28603 9.79689 9.3798 9.70312C9.47357 9.60935 9.60075 9.55667 9.73335 9.55667H12.1267C12.2593 9.55667 12.3865 9.60935 12.4802 9.70312C12.574 9.79689 12.6267 9.92407 12.6267 10.0567C12.6267 10.1893 12.574 10.3165 12.4802 10.4102C12.3865 10.504 12.2593 10.5567 12.1267 10.5567ZM9.23335 7.99668C9.23335 7.86407 9.28603 7.73689 9.3798 7.64312C9.47357 7.54935 9.60075 7.49668 9.73335 7.49668H10.76C10.8926 7.49668 11.0198 7.54935 11.1136 7.64312C11.2073 7.73689 11.26 7.86407 11.26 7.99668C11.26 8.12928 11.2073 8.25646 11.1136 8.35023C11.0198 8.444 10.8926 8.49668 10.76 8.49668H9.73335C9.66745 8.49757 9.60202 8.48525 9.54096 8.46044C9.47989 8.43563 9.42441 8.39884 9.3778 8.35223C9.33119 8.30562 9.2944 8.25014 9.26959 8.18907C9.24478 8.12801 9.23246 8.06258 9.23335 7.99668ZM12.1267 6.44334H9.73335C9.60075 6.44334 9.47357 6.39066 9.3798 6.2969C9.28603 6.20313 9.23335 6.07595 9.23335 5.94334C9.23335 5.81073 9.28603 5.68356 9.3798 5.58979C9.47357 5.49602 9.60075 5.44334 9.73335 5.44334H12.1267C12.2593 5.44334 12.3865 5.49602 12.4802 5.58979C12.574 5.68356 12.6267 5.81073 12.6267 5.94334C12.6267 6.07595 12.574 6.20313 12.4802 6.2969C12.3865 6.39066 12.2593 6.44334 12.1267 6.44334Z" fill="#52CAD1"/><path d="M5.72 7.59002C5.83381 7.59002 5.94651 7.56761 6.05166 7.52405C6.15681 7.4805 6.25235 7.41666 6.33283 7.33618C6.4133 7.2557 6.47714 7.16016 6.52069 7.05501C6.56425 6.94987 6.58667 6.83717 6.58667 6.72336C6.58667 6.60954 6.56425 6.49685 6.52069 6.3917C6.47714 6.28655 6.4133 6.19101 6.33283 6.11053C6.25235 6.03005 6.15681 5.96621 6.05166 5.92266C5.94651 5.87911 5.83381 5.85669 5.72 5.85669C5.49014 5.85669 5.2697 5.948 5.10717 6.11053C4.94464 6.27306 4.85333 6.4935 4.85333 6.72336C4.85333 6.95321 4.94464 7.17365 5.10717 7.33618C5.2697 7.49871 5.49014 7.59002 5.72 7.59002Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1539"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
// 索引4 — 第五个菜单项(待替换)
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_268_1538)"><path d="M6.07134 3.63198C6.47001 2.46531 8.08201 2.42998 8.55468 3.52598L8.59468 3.63265L9.13268 5.20598C9.25597 5.5668 9.45521 5.89699 9.71696 6.17426C9.97871 6.45154 10.2969 6.66945 10.65 6.81331L10.7947 6.86731L12.368 7.40465C13.5347 7.80331 13.57 9.41531 12.4747 9.88798L12.368 9.92798L10.7947 10.466C10.4337 10.5892 10.1034 10.7884 9.82602 11.0502C9.54863 11.3119 9.33061 11.6301 9.18668 11.9833L9.13268 12.1273L8.59534 13.7013C8.19668 14.868 6.58468 14.9033 6.11268 13.808L6.07134 13.7013L5.53401 12.128C5.4108 11.767 5.21159 11.4367 4.94984 11.1593C4.68808 10.8819 4.36987 10.6639 4.01668 10.52L3.87268 10.466L2.29934 9.92865C1.13201 9.52998 1.09668 7.91798 2.19268 7.44598L2.29934 7.40465L3.87268 6.86731C4.2335 6.74402 4.56368 6.54478 4.84096 6.28303C5.11823 6.02128 5.33615 5.70311 5.48001 5.34998L5.53401 5.20598L6.07134 3.63198ZM12.6667 1.33331C12.7914 1.33331 12.9136 1.3683 13.0194 1.43429C13.1253 1.50029 13.2105 1.59465 13.2653 1.70665L13.2973 1.78465L13.5307 2.46865L14.2153 2.70198C14.3403 2.74444 14.4499 2.82306 14.5302 2.92786C14.6104 3.03266 14.6578 3.15893 14.6662 3.29066C14.6746 3.4224 14.6437 3.55367 14.5775 3.66784C14.5112 3.78201 14.4126 3.87394 14.294 3.93198L14.2153 3.96398L13.5313 4.19731L13.298 4.88198C13.2555 5.00693 13.1768 5.11645 13.072 5.19664C12.9671 5.27684 12.8408 5.32411 12.7091 5.33246C12.5774 5.34081 12.4461 5.30988 12.332 5.24357C12.2179 5.17726 12.126 5.07856 12.068 4.95998L12.036 4.88198L11.8027 4.19798L11.118 3.96465C10.993 3.92218 10.8835 3.84357 10.8032 3.73877C10.7229 3.63397 10.6756 3.5077 10.6672 3.37597C10.6587 3.24423 10.6896 3.11296 10.7559 2.99879C10.8221 2.88462 10.9208 2.79269 11.0393 2.73465L11.118 2.70265L11.802 2.46931L12.0353 1.78465C12.0803 1.65293 12.1653 1.53859 12.2786 1.45765C12.3918 1.37671 12.5275 1.33323 12.6667 1.33331Z" fill="#52CAD1"/></g><defs><clipPath id="clip0_268_1538"><rect width="16" height="16" fill="white"/></clipPath></defs></svg>',
// 索引5 — 第六个菜单项(待替换)
'<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.66667 10.6667L1.13333 12.2C0.922223 12.4111 0.680446 12.4584 0.408001 12.342C0.135557 12.2256 -0.000443359 12.0171 1.08578e-06 11.7167V1.33333C1.08578e-06 0.966667 0.130668 0.652889 0.392001 0.392C0.653334 0.131111 0.967112 0.000444444 1.33333 0H12C12.3667 0 12.6807 0.130667 12.942 0.392C13.2033 0.653333 13.3338 0.967111 13.3333 1.33333V9.33333C13.3333 9.7 13.2029 10.014 12.942 10.2753C12.6811 10.5367 12.3671 10.6671 12 10.6667H2.66667ZM6.66667 8.66667C6.85556 8.66667 7.014 8.60267 7.142 8.47467C7.27 8.34667 7.33378 8.18845 7.33333 8C7.33289 7.81156 7.26889 7.65333 7.14133 7.52533C7.01378 7.39733 6.85556 7.33333 6.66667 7.33333C6.47778 7.33333 6.31956 7.39733 6.192 7.52533C6.06445 7.65333 6.00045 7.81156 6 8C5.99956 8.18845 6.06356 8.34689 6.192 8.47533C6.32045 8.60378 6.47867 8.66756 6.66667 8.66667ZM7.142 5.808C7.26956 5.68044 7.33333 5.52222 7.33333 5.33333V2.66667C7.33333 2.47778 7.26933 2.31956 7.14133 2.192C7.01333 2.06444 6.85511 2.00044 6.66667 2C6.47822 1.99956 6.32 2.06356 6.192 2.192C6.064 2.32044 6 2.47867 6 2.66667V5.33333C6 5.52222 6.064 5.68067 6.192 5.80867C6.32 5.93667 6.47822 6.00044 6.66667 6C6.85511 5.99956 7.01356 5.93556 7.142 5.808Z" fill="#52CAD1"/></svg>',
]
/**
* 根据菜单索引获取选中状态图标
* 当菜单处于 hover 或 active 状态时使用此图标替换默认图标
*/
function getActiveIcon(index: number): string {
return activeIconSvgs[index] || ''
}
const route = useRoute()
const router = useRouter()
const store = useStore()
/**
* 监听路由变化:已登录时刷新用户信息和会员状态到 vuex
* 这样每次页面切换都能拿到最新数据
*/
watch(() => route.path, () => {
if (store.state.isAuthenticated) {
store.dispatch('loadUserInfo')
store.dispatch('loadMemberStatus')
}
}, { immediate: true })
interface MenuItem {
name: string
path: string
iconImg: string
label: string
badge?: string
/** 排序字段 */
sortOrder: number
/** 是否有使用权限 */
accessible: boolean
}
/**
* 图标映射表(兼容旧版字符串 key,新版后端直接返回 SVG 字符串)
* 如果 meta.icon 是一个已知的 key,则使用对应图片;否则直接当作 SVG 字符串使用
*/
const iconMap: Record<string, string> = {
'nav-jobs-icon': navJobsIcon,
'nav-resume-icon': navResumeIcon,
'nav-profile-icon': navProfileIcon,
'nav-agent-icon': navAgentIcon,
'nav-setting-icon': navSettingIcon,
'nav-mentor-icon': navMentorIcon,
}
/**
* 解析图标:如果是已知的 key 则返回 img 标签,如果是 SVG 字符串则直接返回
*/
function resolveIcon(iconValue: string | undefined): string {
if (!iconValue) return ''
// 如果在映射表中找到了,说明是旧版 key,包装成 img 标签
if (iconMap[iconValue]) {
return `<img src="${iconMap[iconValue]}" alt="" />`
}
// 否则认为是 SVG 字符串,直接返回
return iconValue
}
/**
* 静态菜单 — 不需要登录就能看到的导航项(比如"职位")
* 这些菜单始终显示,不依赖后端返回
* sortOrder 设为 0 确保在未获取到后端数据时也能正常显示
*/
const staticMenus: MenuItem[] = [
{ name: 'Jobs', path: '/jobs', iconImg: `<img src="${navJobsIcon}" alt="" />`, label: '职位', sortOrder: 0, accessible: true },
]
/**
* 动态菜单 — 从 store.state.dynamicMenus(后端返回的数据)转换而来
* 登录后才会有数据,登出后自动清空
* 过滤掉 position === 'footer' 的项(如"设置"),它们显示在底部区域
* 按 sortOrder 排序,routeNamemeta.label)作为显示名称
*/
const dynamicMenuItems = computed<MenuItem[]>(() => {
return store.state.dynamicMenus
.filter((item: any) => item.meta?.position !== 'footer')
.map((item: any) => ({
name: item.name,
path: item.path,
iconImg: resolveIcon(item.meta?.icon),
label: item.meta?.label || item.name,
badge: item.meta?.badge,
sortOrder: item.sortOrder ?? 99,
accessible: item.accessible !== false, // 默认为 true
}))
.sort((a: MenuItem, b: MenuItem) => a.sortOrder - b.sortOrder)
})
/**
* 最终渲染的主菜单 = 静态菜单 + 动态菜单,按 sortOrder 统一排序
* 如果后端返回了 Jobs 的数据,用后端数据覆盖静态菜单的 sortOrder
*/
const mainMenus = computed<MenuItem[]>(() => {
// 动态菜单中已包含后端返回的 Jobs(如果有的话),此时不再使用静态的
const dynamicNames = dynamicMenuItems.value.map(m => m.name)
const filteredStatic = staticMenus.filter(s => !dynamicNames.includes(s.name))
return [...filteredStatic, ...dynamicMenuItems.value].sort((a, b) => a.sortOrder - b.sortOrder)
})
const showShareDialog = ref(false)
const showMessageDialog = ref(false)
const showFeedbackDialog = ref(false)
/** 当前hover的菜单索引,-1表示无hover */
const hoverIndex = ref(-1)
/** 会员权限拦截弹窗 */
const showMemberAccessDialog = ref(false)
/** 会员购买弹窗 */
const showMemberDialog = ref(false)
const showSettingsDialog = computed({
get: () => store.state.showSettings,
set: (val: boolean) => store.commit('SET_SHOW_SETTINGS', val),
})
const feedbackType = ref('')
const feedbackTypeIndex = ref(0)
const feedbackDetail = ref('')
import { ElMessage } from 'element-plus'
import SettingsInviteDialog from "@/components/SettingsInviteDialog.vue";
// ==================== 站内信相关 ====================
/** 未读消息数量 */
const unreadCount = ref(0)
// ==================== 邀请统计相关 ====================
/** 当前周期已邀请人数 */
const milestoneProgress = ref(0)
/** 里程碑目标总人数(从接口获取) */
const milestoneTarget = ref(30)
/** 进度条百分比 */
const inviteProgressPercent = computed(() => {
if (milestoneTarget.value <= 0) return 0
return Math.min(Math.round((milestoneProgress.value / milestoneTarget.value) * 100), 100)
})
/** 还需邀请人数 */
const inviteRemaining = computed(() => {
return Math.max(milestoneTarget.value - milestoneProgress.value, 0)
})
/**
* 获取邀请统计数据
*/
async function loadInviteStats() {
try {
const res = await fetchInviteStats()
milestoneProgress.value = res.data?.milestoneProgress ?? 0
milestoneTarget.value = res.data?.milestoneTarget ?? 30
} catch (e) {
console.error('获取邀请统计失败', e)
}
}
/** 消息列表数据 */
const messageList = ref<MessageDto[]>([])
/** 当前选中的消息索引 */
const selectedMessageIdx = ref(0)
/** 当前页码 */
const messagePageNum = ref(1)
/** 是否还有更多数据 */
const messageHasMore = ref(true)
/** 加载状态 */
const messageLoading = ref(false)
/** 当前选中的消息对象 */
const currentMessage = computed(() => {
return messageList.value[selectedMessageIdx.value] || null
})
/**
* 获取未读消息数量
*/
async function loadUnreadCount() {
try {
const res = await fetchUnreadCount()
// 接口返回结构: { code, data: number }
unreadCount.value = res.data ?? 0
} catch (e) {
console.error('获取未读消息数量失败', e)
}
}
// 组件挂载时获取未读消息数量(已移到下方 onMounted 统一处理)
/**
* 加载消息列表(分页追加)
*/
async function loadMessages() {
if (messageLoading.value || !messageHasMore.value) return
messageLoading.value = true
try {
const res = await fetchMessageList({ pageNum: messagePageNum.value, pageSize: 20 })
// 接口返回结构: { code, msg, data: { pageNum, pageSize, total, list } }
const pageData = res.data
if (pageData && pageData.list && pageData.list.length > 0) {
messageList.value.push(...pageData.list)
// 判断是否还有更多
messageHasMore.value = messageList.value.length < pageData.total
messagePageNum.value++
} else {
messageHasMore.value = false
}
} catch (e) {
console.error('加载消息失败', e)
} finally {
messageLoading.value = false
}
}
/**
* 消息列表滚动到底部时加载下一页
*/
function handleMessageListScroll(e: Event) {
const el = e.target as HTMLElement
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 10) {
loadMessages()
}
}
/**
* 刷新消息列表数据(保持当前选中的消息不变)
*/
async function refreshMessageList(keepMessageId: number | string) {
try {
// 重新请求已加载的所有页数据
const totalLoaded = messageList.value.length
const res = await fetchMessageList({ pageNum: 1, pageSize: totalLoaded })
const pageData = res.data
if (pageData && pageData.list) {
messageList.value = pageData.list
// 根据之前选中的消息ID恢复选中状态
const idx = messageList.value.findIndex((m: MessageDto) => String(m.id) === String(keepMessageId))
selectedMessageIdx.value = idx >= 0 ? idx : 0
}
} catch (e) {
console.error('刷新消息列表失败', e)
}
}
/**
* 上报消息已读:当右侧显示某条消息时,如果该消息未读则标记已读
* 标记成功后刷新列表数据和未读数量
*/
async function handleMarkRead(msg: MessageDto) {
if (msg.read) return
try {
await markMessageRead(msg.id)
// 刷新列表状态,保持当前选中
await refreshMessageList(msg.id)
// 更新未读数量
loadUnreadCount()
} catch (e) {
console.error('标记已读失败', e)
}
}
/**
* 监听当前选中消息变化,触发已读上报
*/
watch(currentMessage, (msg) => {
if (msg && !msg.read) {
handleMarkRead(msg)
}
})
/**
* 监听弹窗打开,重置并加载第一页消息
*/
watch(showMessageDialog, (val) => {
if (val) {
messageList.value = []
messagePageNum.value = 1
messageHasMore.value = true
selectedMessageIdx.value = 0
loadMessages()
}
})
const feedbackOptions = ['Bug反馈', '功能建议', '使用体验', '订阅及会员权益相关问题', '其它']
/**
* 提交反馈
*/
async function handleFeedbackSubmit() {
console.log('反馈类型:', feedbackType.value,'类型index', feedbackTypeIndex.value, '详情:', feedbackDetail.value)
if(feedbackTypeIndex.value<1||feedbackDetail.value.trim()==''){
ElMessage.warning('请选择问题并填写反馈内容')
}else{
//反馈接口
await userFeedback({
type: feedbackTypeIndex.value,
content: feedbackDetail.value
})
ElMessage.success('提交成功')
feedbackType.value = ''
feedbackDetail.value = ''
feedbackTypeIndex.value = 0
showFeedbackDialog.value = false
}
}
/**
* 底部"设置"按钮 从动态菜单中取 position === 'footer' 的项
* 未登录时使用默认图标和文字
*/
const settingsMenu = computed(() => {
const item = store.state.dynamicMenus.find((m: any) => m.meta?.position === 'footer')
if (item) {
return {
iconImg: resolveIcon(item.meta?.icon) || `<img src="${navSettingIcon}" alt="" />`,
label: item.meta?.label || '设置',
path: item.path,
}
}
return { iconImg: `<img src="${navSettingIcon}" alt="" />`, label: '设置', path: '/settings' }
})
const footerMenus = computed(() => [
{ iconImg: `<img src="${navMessageIcon}" alt="" />`, label: '消息通知', badge: unreadCount.value > 0 ? 'NEW' : '', action: () => { showMessageDialog.value = true } },
{ iconImg: settingsMenu.value.iconImg, label: settingsMenu.value.label, action: () => { handleSettingsNav() } },
{ iconImg: `<img src="${navFeedbackIcon}" alt="" />`, label: '反馈', action: () => { showFeedbackDialog.value = true } },
])
/**
* 手机号脱敏显示:134****1254 格式
*/
const maskedPhone = computed(() => {
const phone = store.state.userInfo?.mobileNumber || ''
if (phone.length >= 7) {
return phone.slice(0, 3) + '****' + phone.slice(-4)
}
return phone || '未绑定手机'
})
/**
* 会员标签文字:根据 isMember + memberType 判断
* memberType: 1=正式会员 2=试用会员, isMember=false → 免费用户
*/
const memberTagText = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return '免费用户'
if (status.memberType === 1) return '正式会员'
if (status.memberType === 2) return '试用会员'
return '免费用户'
})
/**
* 账户弹窗右上角按钮文字:免费用户显示"开通会员",付费用户显示"详情>"
*/
const accountDetailBtnText = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return '开通会员'
return '详情>'
})
/**
* 会员标签样式 class
*/
const memberTagClass = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return 'side-nav__user-member-tag--free'
if (status.memberType === 1) return 'side-nav__user-member-tag--formal'
if (status.memberType === 2) return 'side-nav__user-member-tag--trial'
return 'side-nav__user-member-tag--free'
})
/** 账户弹窗显示状态 */
const showAccountPopup = ref(false)
/** 退出登录确认弹窗 */
const showLogoutConfirm = ref(false)
/**
* 账户弹窗中会员类型字体颜色 class
*/
const accountMemberColorClass = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return 'side-nav__account-popup-member-type--free'
if (status.memberType === 1) return 'side-nav__account-popup-member-type--formal'
if (status.memberType === 2) return 'side-nav__account-popup-member-type--trial'
return 'side-nav__account-popup-member-type--free'
})
/**
* 账户弹窗顶部渐变背景 class
* 正式会员:#EDF9FA → #FFF;试用会员:#FEF5E7 → #FFF;免费用户:#F1F2F4 → #FFF
*/
const accountHeaderBgClass = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return 'side-nav__account-popup-header--free'
if (status.memberType === 1) return 'side-nav__account-popup-header--formal'
if (status.memberType === 2) return 'side-nav__account-popup-header--trial'
return 'side-nav__account-popup-header--free'
})
/**
* 会员到期时间显示文字 — 使用项目时间工具 timestampToLocalDateTime
*/
const memberExpireText = computed(() => {
const status = store.state.memberStatus
if (!status || !status.isMember) return ''
if (status.expireTime) {
return timestampToLocalDateTime(status.expireTime, 'returnDay') + '到期'
}
return ''
})
/** 账户弹窗 — 点击"详情>"跳转会员页 */
/** 账户弹窗 — 点击"详情>":正式会员打开设置弹窗并切换到会员tab,试用/免费用户打开会员购买弹窗 */
function handleAccountDetail() {
showAccountPopup.value = false
const status = store.state.memberStatus
if (status && status.isMember && status.memberType === 1) {
// 正式会员:打开设置弹窗,切换到会员页
store.state.settingsTab = 'member'
showSettingsDialog.value = true
} else {
// 试用会员 / 免费用户:打开会员购买弹窗
showMemberDialog.value = true
}
}
/** 账户弹窗 — 点击"设置"打开设置弹窗 */
function handleAccountSettings() {
showAccountPopup.value = false
handleSettingsNav()
}
/** 账户弹窗 — 点击"退出登录"弹出确认弹窗 */
function handleAccountLogout() {
showAccountPopup.value = false
showLogoutConfirm.value = true
}
/** 确认退出登录 */
async function confirmLogout() {
showLogoutConfirm.value = false
try {
const res = await logout()
if (res.code === '0') {
ElMessage.success('已退出登录')
}
} catch {
// 即使接口失败也执行前端登出
}
store.dispatch('logout')
router.push('/')
}
/**
* 点击页面其他地方关闭账户弹窗
*/
function handleGlobalClick() {
if (showAccountPopup.value) {
showAccountPopup.value = false
}
}
onMounted(() => {
loadUnreadCount()
loadInviteStats()
document.addEventListener('click', handleGlobalClick)
})
onUnmounted(() => {
document.removeEventListener('click', handleGlobalClick)
})
/**
* 设置弹窗:需要登录,通过 checkLogin 接口验证
*/
async function handleSettingsNav() {
try {
const res = await checkLogin()
if (res.code === '0' && res.data === true) {
store.commit('SET_AUTHENTICATED', true)
store.commit('SET_SETTINGS_TAB', 'account')
showSettingsDialog.value = true
} else {
store.commit('SET_AUTHENTICATED', false)
store.dispatch('openLogin')
}
} catch {
store.commit('SET_AUTHENTICATED', false)
store.dispatch('openLogin')
}
}
/**
* 导航点击处理:
* - 静态页面(Jobs)直接跳转
* - accessible 为 false 的菜单 → 弹出会员权限拦截弹窗
* - 动态页面通过 checkLogin 接口验证,未登录则弹登录框
*/
const staticNames = staticMenus.map(m => m.name)
async function handleNav(item: MenuItem) {
// 无权限菜单 — 弹出会员权限拦截弹窗
if (!item.accessible) {
showMemberAccessDialog.value = true
return
}
// 外部链接 — routePath 是 http/https 开头,新标签页打开
if (/^https?:\/\//.test(item.path)) {
window.open(item.path, '_blank')
return
}
// Jobs 等静态页面 — 无需登录验证,直接跳转
if (staticNames.includes(item.name)) {
router.push(item.path)
return
}
// 动态页面 — 需要登录验证
try {
const res = await checkLogin()
if (res.code === '0' && res.data === true) {
store.commit('SET_AUTHENTICATED', true)
router.push(item.path)
} else {
store.commit('SET_AUTHENTICATED', false)
store.dispatch('openLogin', item.path)
}
} catch {
store.commit('SET_AUTHENTICATED', false)
store.dispatch('openLogin', item.path)
}
}
function isActive(name: string) {
return route.name === name
}
</script>