2026-01-16 22:04:12 +08:00
|
|
|
<template>
|
|
|
|
|
<el-menu :default-active="state.activeMenu" @select="onSelect">
|
2026-04-07 11:01:11 +08:00
|
|
|
<!-- 语言切换 -->
|
|
|
|
|
<el-sub-menu
|
|
|
|
|
v-if="configStore.language.is_enabled && availableLanguages.length > 1"
|
|
|
|
|
index="language-switch"
|
|
|
|
|
class="language-switch"
|
|
|
|
|
:key="languageKey"
|
|
|
|
|
>
|
2026-01-16 22:04:12 +08:00
|
|
|
<template #title>
|
2026-04-07 11:01:11 +08:00
|
|
|
<Icon v-if="showIcon" icon="ant-design:translation-outlined" color="var(--el-text-color-primary)" />
|
|
|
|
|
{{ t("common.language") }}
|
2026-01-16 22:04:12 +08:00
|
|
|
</template>
|
2026-04-07 11:01:11 +08:00
|
|
|
|
2026-01-16 22:04:12 +08:00
|
|
|
<el-menu-item
|
2026-04-07 11:01:11 +08:00
|
|
|
@click="handleLanguageChange(item.name)"
|
2026-01-16 22:04:12 +08:00
|
|
|
v-for="item in availableLanguages"
|
|
|
|
|
:key="item.name"
|
2026-04-07 11:01:11 +08:00
|
|
|
:index="'language-switch-' + item.name"
|
2026-01-16 22:04:12 +08:00
|
|
|
class="language-switch"
|
2026-04-07 11:01:11 +08:00
|
|
|
:class="{ 'is-active': item.name === currentLanguage }"
|
|
|
|
|
:title="`item.name: ${item.name}, currentLanguage: ${currentLanguage}`"
|
2026-01-16 22:04:12 +08:00
|
|
|
>
|
|
|
|
|
{{ item.value }}
|
|
|
|
|
</el-menu-item>
|
|
|
|
|
</el-sub-menu>
|
|
|
|
|
|
|
|
|
|
<!-- 主题切换 -->
|
|
|
|
|
<el-menu-item
|
2026-04-07 11:01:11 +08:00
|
|
|
v-if="configStore.theme.is_enabled"
|
2026-01-16 22:04:12 +08:00
|
|
|
index="theme-switch"
|
|
|
|
|
class="theme-switch"
|
|
|
|
|
:class="$attrs.mode + '-theme-switch'"
|
|
|
|
|
>
|
|
|
|
|
<DarkSwitch />
|
|
|
|
|
<!-- <DarkToggle v-if="!isMobile" /> -->
|
|
|
|
|
</el-menu-item>
|
|
|
|
|
|
|
|
|
|
<!-- 已登录用户场景 -->
|
|
|
|
|
<el-sub-menu
|
|
|
|
|
v-if="memberStore.info"
|
|
|
|
|
index="user-box"
|
|
|
|
|
>
|
|
|
|
|
<template #title>
|
|
|
|
|
<div class="header-user-box">
|
|
|
|
|
<img
|
|
|
|
|
class="header-user-avatar"
|
|
|
|
|
:class="$attrs.mode == 'vertical' ? 'icon-header-user-avatar' : ''"
|
|
|
|
|
:src="getAvatarUrl(memberStore?.info?.avatar || '')"
|
|
|
|
|
alt=""
|
|
|
|
|
/>
|
2026-04-07 11:01:11 +08:00
|
|
|
{{ memberStore?.info?.nickname || t("common.user") }}
|
2026-01-16 22:04:12 +08:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
<el-menu-item index="user" @click="$emit('menu-click'); navigateTo('/member/profile')">
|
2026-01-16 22:04:12 +08:00
|
|
|
<Icon
|
|
|
|
|
v-if="showIcon"
|
2026-04-07 11:01:11 +08:00
|
|
|
icon="ant-design:user-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
color="var(--el-text-color-primary)"
|
|
|
|
|
/>
|
2026-04-07 11:01:11 +08:00
|
|
|
{{ t("common.member_center") }}
|
2026-01-16 22:04:12 +08:00
|
|
|
</el-menu-item>
|
|
|
|
|
|
|
|
|
|
<!-- 会员中心菜单 -->
|
2026-04-07 11:01:11 +08:00
|
|
|
<MenuSub :menus="systemStore.site.member_menu" :show-icon="showIcon" @menu-click="$emit('menu-click')" />
|
2026-01-16 22:04:12 +08:00
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
<el-menu-item @click="$emit('menu-click'); memberStore.logout()" index="user-logout">
|
2026-01-16 22:04:12 +08:00
|
|
|
<Icon
|
|
|
|
|
v-if="showIcon"
|
2026-04-07 11:01:11 +08:00
|
|
|
icon="ant-design:logout-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
color="var(--el-text-color-primary)"
|
|
|
|
|
/>
|
2026-04-07 11:01:11 +08:00
|
|
|
{{ t("common.logout_login") }}
|
2026-01-16 22:04:12 +08:00
|
|
|
</el-menu-item>
|
|
|
|
|
</el-sub-menu>
|
|
|
|
|
|
|
|
|
|
<!-- 未登录场景 -->
|
2026-04-07 11:01:11 +08:00
|
|
|
<el-menu-item v-else @click="handleLoginClick" index="user">
|
2026-01-16 22:04:12 +08:00
|
|
|
<Icon
|
|
|
|
|
v-if="showIcon"
|
2026-04-07 11:01:11 +08:00
|
|
|
icon="ant-design:user-outlined"
|
2026-01-16 22:04:12 +08:00
|
|
|
color="var(--el-text-color-primary)"
|
|
|
|
|
/>
|
2026-04-07 11:01:11 +08:00
|
|
|
{{ t("common.login") }}
|
2026-01-16 22:04:12 +08:00
|
|
|
</el-menu-item>
|
|
|
|
|
<LoadingDialog />
|
|
|
|
|
|
|
|
|
|
</el-menu>
|
|
|
|
|
</template>
|
|
|
|
|
<script setup lang="ts">
|
2026-04-07 11:01:11 +08:00
|
|
|
import { computed, reactive, ref, nextTick, onMounted, onUnmounted, watch } from "vue";
|
|
|
|
|
import { navigateTo } from "nuxt/app";
|
2026-01-16 22:04:12 +08:00
|
|
|
import type { Menus } from "~/stores/interface";
|
|
|
|
|
import defaultAvatar from "~/assets/images/default_avatar.png";
|
2026-04-07 11:01:11 +08:00
|
|
|
import { fullUrl, getToken } from "~/utils/common";
|
2026-01-16 22:04:12 +08:00
|
|
|
import DarkToggle from "./dark-toggle.vue";
|
|
|
|
|
import DarkSwitch from "./dark-switch.vue";
|
|
|
|
|
import LoadingDialog from "@/components/login-dialog/index.vue";
|
|
|
|
|
import MenuSub from "./menu-sub.vue";
|
2026-04-07 11:01:11 +08:00
|
|
|
import { Icon } from '~/components/icon';
|
|
|
|
|
import { ElMessage } from "element-plus";
|
|
|
|
|
import { useMemberStore } from "~/stores/member";
|
|
|
|
|
import { useSystemStore } from "~/stores/system";
|
|
|
|
|
import { useConfigStore } from "~/stores/config";
|
|
|
|
|
import { t } from "~/composables/lang";
|
|
|
|
|
import { getDark, setDark } from "~/utils/dark";
|
|
|
|
|
|
2026-01-16 22:04:12 +08:00
|
|
|
|
|
|
|
|
const memberStore = useMemberStore();
|
|
|
|
|
const systemStore = useSystemStore();
|
|
|
|
|
const configStore = useConfigStore();
|
2026-04-07 11:01:11 +08:00
|
|
|
|
|
|
|
|
// 添加响应式的语言 key,用于强制组件重新渲染
|
|
|
|
|
const languageKey = ref(systemStore.lang);
|
|
|
|
|
|
|
|
|
|
// 监听语言变化,更新 languageKey
|
|
|
|
|
watch(() => systemStore.lang, (newLang, oldLang) => {
|
|
|
|
|
if (newLang !== oldLang) {
|
|
|
|
|
languageKey.value = newLang;
|
|
|
|
|
}
|
|
|
|
|
}, { immediate: true });
|
|
|
|
|
|
|
|
|
|
const availableLanguages = computed(() => {
|
|
|
|
|
const languages = systemStore.getLanguages;
|
|
|
|
|
return languages;
|
|
|
|
|
});
|
|
|
|
|
const currentLanguage = computed(() => {
|
|
|
|
|
// 从 systemStore 中获取语言设置,确保与状态保持同步
|
|
|
|
|
return systemStore.lang;
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-16 22:04:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
showIcon?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
2026-04-07 11:01:11 +08:00
|
|
|
showIcon: true,
|
2026-01-16 22:04:12 +08:00
|
|
|
});
|
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
const emit = defineEmits<{
|
|
|
|
|
'menu-click': []
|
|
|
|
|
}>();
|
|
|
|
|
|
2026-01-16 22:04:12 +08:00
|
|
|
const state = reactive({
|
|
|
|
|
activeMenu: "",
|
|
|
|
|
});
|
|
|
|
|
// 判断是否为移动端
|
|
|
|
|
const isMobile = ref(false);
|
|
|
|
|
|
|
|
|
|
const checkMobile = () => {
|
|
|
|
|
isMobile.value = window.innerWidth <= 768;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取用户头像URL
|
|
|
|
|
* @param avatarUrl 用户头像URL,可为空、null或undefined
|
|
|
|
|
* @returns 完整的头像URL,如果传入空值则返回默认头像
|
|
|
|
|
*/
|
|
|
|
|
const getAvatarUrl = (avatarUrl: string | null | undefined): string => {
|
|
|
|
|
if (!avatarUrl || avatarUrl.trim() === "") {
|
|
|
|
|
// 返回默认头像
|
|
|
|
|
return defaultAvatar;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fullUrl(avatarUrl);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleLogin = () => {
|
|
|
|
|
if (
|
|
|
|
|
!getToken() &&
|
|
|
|
|
!configStore.login.is_username &&
|
|
|
|
|
!configStore.login.is_mobile &&
|
|
|
|
|
!configStore.login.is_bind_mobile
|
|
|
|
|
) {
|
|
|
|
|
ElMessage.error("商家未开启普通账号登录注册");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
memberStore.logOpen();
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-07 11:01:11 +08:00
|
|
|
const handleLoginClick = () => {
|
|
|
|
|
handleLogin();
|
|
|
|
|
$emit('menu-click');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理语言切换
|
|
|
|
|
* @param lang 语言代码
|
|
|
|
|
*/
|
|
|
|
|
const handleLanguageChange = (lang: string) => {
|
|
|
|
|
const success = systemStore.setLanguage(lang);
|
|
|
|
|
|
|
|
|
|
// 强制更新 languageKey,触发组件重新渲染
|
|
|
|
|
languageKey.value = lang;
|
|
|
|
|
|
|
|
|
|
// 不再刷新页面,语言切换应该立即生效
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-16 22:04:12 +08:00
|
|
|
/**
|
|
|
|
|
* 菜单被点击时额外对无需激活的菜单处理(外链、暗黑模式开关、语言切换等)
|
|
|
|
|
* 检查菜单是否需要激活,如果否,还原 state.activeMenu
|
|
|
|
|
*/
|
|
|
|
|
const onSelect = (index: string) => {
|
|
|
|
|
if (noNeedActive(systemStore.site.head_nav, index)) {
|
|
|
|
|
const oldActiveMenu = state.activeMenu;
|
|
|
|
|
state.activeMenu = "";
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
state.activeMenu = oldActiveMenu;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 检查一个菜单是否需要激活态
|
|
|
|
|
* @param menus
|
|
|
|
|
* @param index
|
|
|
|
|
*/
|
|
|
|
|
const noNeedActive = (menus: Menus[], index: string) => {
|
|
|
|
|
if (index.indexOf("language-switch") === 0 || index == "theme-switch") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return isExternalLink(menus, index);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 检查一个菜单是否是外站链接,如果是,不要激活
|
|
|
|
|
* @param menus
|
|
|
|
|
* @param index
|
|
|
|
|
*/
|
|
|
|
|
const isExternalLink = (menus: Menus[], index: string): boolean => {
|
|
|
|
|
// 修复:添加空值检查
|
|
|
|
|
if (!menus || !Array.isArray(menus)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const key in menus) {
|
|
|
|
|
// 修复:检查 menus[key] 是否存在
|
|
|
|
|
if (!menus[key]) continue;
|
|
|
|
|
|
|
|
|
|
const columnIndex = `column-${menus[key].id}`;
|
|
|
|
|
if (columnIndex == index) {
|
|
|
|
|
return menus[key].meta?.menu_type == "link";
|
|
|
|
|
}
|
|
|
|
|
// 修复:添加 children 存在性检查
|
|
|
|
|
if (
|
|
|
|
|
menus[key].children &&
|
|
|
|
|
Array.isArray(menus[key].children) &&
|
|
|
|
|
menus[key].children.length
|
|
|
|
|
) {
|
|
|
|
|
const result = isExternalLink(menus[key].children, index);
|
|
|
|
|
if (result) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
const toggleDarkMode = () => {
|
|
|
|
|
setDark(!getDark());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
checkMobile();
|
|
|
|
|
window.addEventListener("resize", checkMobile);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
window.removeEventListener("resize", checkMobile);
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.header-user-box {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
position: relative;
|
|
|
|
|
.header-user-avatar {
|
|
|
|
|
width: 25px;
|
|
|
|
|
height: 25px;
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background-color: black;
|
|
|
|
|
}
|
|
|
|
|
.icon-header-user-avatar {
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.el-sub-menu .icon,
|
|
|
|
|
.el-menu-item .icon {
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
margin-right: 2px;
|
|
|
|
|
width: 24px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
.is-active > .icon {
|
|
|
|
|
color: var(--el-menu-active-color) !important;
|
|
|
|
|
}
|
|
|
|
|
.el-menu {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
border-right: none;
|
|
|
|
|
.theme-switch.is-active,
|
|
|
|
|
.language-switch.is-active {
|
|
|
|
|
border-bottom: none;
|
2026-04-07 11:01:11 +08:00
|
|
|
color: var(--el-menu-active-color) !important;
|
|
|
|
|
font-weight: 500 !important;
|
2026-01-16 22:04:12 +08:00
|
|
|
:deep(.el-sub-menu__title) {
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.theme-switch {
|
|
|
|
|
--el-menu-hover-bg-color: none;
|
|
|
|
|
padding-right: 0;
|
2026-04-07 11:01:11 +08:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
2026-01-16 22:04:12 +08:00
|
|
|
}
|
|
|
|
|
.vertical-theme-switch {
|
|
|
|
|
.theme-toggle-content {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.theme-toggle-content {
|
|
|
|
|
padding-right: 0;
|
|
|
|
|
}
|
2026-04-07 11:01:11 +08:00
|
|
|
|
|
|
|
|
// 语言切换选中状态
|
|
|
|
|
.language-switch.is-active {
|
|
|
|
|
color: var(--el-menu-active-color) !important;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
2026-01-16 22:04:12 +08:00
|
|
|
</style>
|