This commit is contained in:
oneao committed 2025-06-18 16:30:01 +08:00
1 parent bcc4722efc
commit f48536774f
403 files changed
+34776

No files matched your search

@@ -0,0 +1,20 @@
<template>
<div class="layout">
<Topbar></Topbar>
<Center></Center>
<!-- <Footer></Footer> -->
</div>
</template>
<script setup lang="ts">
import Topbar from "../topbar/index.vue";
import Center from "../center/index.vue";
// import Footer from "../footer/index.vue";
</script>
<style lang="scss" scoped>
.layout {
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,7 @@
<template>
<router-view></router-view>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped></style>
@@ -0,0 +1,14 @@
<template>
<div class="footer">footer</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.footer {
width: 100%;
padding-bottom: 20px;
background: #efeff1;
margin-top: 88px;
}
</style>
@@ -0,0 +1,45 @@
<template>
<a-badge :count="imMessageStore.allNoReadNumber" :max-count="99" class="badge" @click="handleToIm">
<div class="i-material-symbols:notifications-outline icon"></div>
</a-badge>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { useImMessageStore } from "@/store/imMessageStore";
import { getAllNoReadNumberApi } from "@/service/message";
import { WebSocketType } from "@/types/websocket";
import { useWebSocketStore } from "@/store/webSocketStore";
const router = useRouter();
const imMessageStore = useImMessageStore();
const webSocketStore = useWebSocketStore();
const unlisten = ref<() => void>(); // 类型为 undefined | (() => void)
onMounted(() => {
getAllNoReadNumberApi({}).then((res) => {
imMessageStore.setAllNoReadNumber(res.data.noReadNumber);
});
unlisten.value = webSocketStore.onMessage((data) => {
if (data.type === WebSocketType.AcceptMessageEnums.ONE) {
imMessageStore.setAllNoReadNumber(imMessageStore.allNoReadNumber + 1);
}
});
});
onUnmounted(() => {
unlisten.value?.();
});
function handleToIm() {
router.push("/im");
}
</script>
<style lang="scss" scoped>
.icon {
font-size: 26px;
cursor: pointer;
}
</style>
@@ -0,0 +1,45 @@
<template>
<a-badge :count="imMessageStore.allNoReadNumber" :max-count="99" class="badge" @click="handleToMessages">
<div class="i-material-symbols:notifications-outline icon"></div>
</a-badge>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { useImMessageStore } from "@/store/imMessageStore";
import { getAllNoReadNumberApi } from "@/service/message";
import { WebSocketType } from "@/types/websocket";
import { useWebSocketStore } from "@/store/webSocketStore";
const router = useRouter();
const imMessageStore = useImMessageStore();
const webSocketStore = useWebSocketStore();
const unlisten = ref<() => void>(); // 类型为 undefined | (() => void)
onMounted(() => {
getAllNoReadNumberApi({}).then((res) => {
imMessageStore.setAllNoReadNumber(res.data.noReadNumber);
});
unlisten.value = webSocketStore.onMessage((data) => {
if (data.type === WebSocketType.AcceptMessageEnums.ONE) {
imMessageStore.setAllNoReadNumber(imMessageStore.allNoReadNumber + 1);
}
});
});
onUnmounted(() => {
unlisten.value?.();
});
function handleToMessages() {
router.push("/messages");
}
</script>
<style lang="scss" scoped>
.icon {
font-size: 22px;
cursor: pointer;
}
</style>
@@ -0,0 +1,59 @@
<template>
<a-button v-if="!userStore.isLogined" type="primary" shape="round" @click="handleToLogin()">登录/注册</a-button>
<a-dropdown v-else>
<div class="user-message">
<a-avatar class="user-message-avatar">
<img src="/RightAvatar.png" />
</a-avatar>
<div class="user-message-name">{{ userStore.userInfo.name }}</div>
</div>
<template #content>
<a-doption @click="handleToUser">
<template #icon>
<SvgIcon icon="material-symbols:settings-outline-rounded"></SvgIcon>
</template>
个人设置
</a-doption>
<a-doption @click="handleToLogin()">
<template #icon>
<SvgIcon icon="material-symbols:exit-to-app"></SvgIcon>
</template>
退出登录
</a-doption>
</template>
</a-dropdown>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import { useUserStore } from "@/store/user";
const router = useRouter();
const userStore = useUserStore();
function handleToLogin() {
userStore.clearUserInfo();
router.push("/login");
}
function handleToUser() {
router.push("/user");
}
</script>
<style lang="scss" scoped>
.user-message {
display: flex;
gap: 8px;
align-items: center;
border-radius: 4px;
padding: 6px 10px;
}
.user-message:hover {
background-color: #ececed;
}
.user-message-avatar {
height: 26px;
width: 26px;
}
</style>
@@ -0,0 +1,60 @@
<template>
<a-button v-if="!userStore.isLogined" type="primary" shape="round" @click="handleToLogin()">登录/注册</a-button>
<a-dropdown v-else>
<div class="user-message">
<SafeImage :url="userStore.userInfo.avatar" class="user-message-avatar" />
<div class="user-message-name">{{ userStore.userInfo.name }}</div>
</div>
<template #content>
<a-doption @click="handleToUser">
<template #icon>
<SvgIcon icon="material-symbols:settings-outline-rounded"></SvgIcon>
</template>
个人设置
</a-doption>
<a-doption @click="handleToLogin()">
<template #icon>
<SvgIcon icon="material-symbols:exit-to-app"></SvgIcon>
</template>
退出登录
</a-doption>
</template>
</a-dropdown>
</template>
<script setup lang="ts">
import SafeImage from "@/components/safe-image/index.vue";
import { useUserStore } from "@/store/user";
import { useRouter } from "vue-router";
const router = useRouter();
const userStore = useUserStore();
function handleToLogin() {
userStore.clearUserInfo();
router.push("/login");
}
function handleToUser() {
router.push("/settings");
}
</script>
<style lang="scss" scoped>
.user-message {
display: flex;
gap: 8px;
align-items: center;
border-radius: 4px;
padding: 6px 10px;
}
.user-message:hover {
background-color: #ececed;
}
.user-message-avatar {
height: 26px;
width: 26px;
border-radius: 50%;
}
</style>
@@ -0,0 +1,186 @@
<template>
<div class="topbar" :class="{ 'at-top': isAtTop }">
<div class="left">
<img src="/logo.png" class="logo" alt="Logo" @click="handleToPath('/home')" />
</div>
<div class="center">
<div class="nav">
<div class="nav-item" :class="{ active: activeNavItem === '/home' }" @click="handleToPath('/home')">
首页
<div class="underline"></div>
</div>
<div class="nav-item" :class="{ active: activeNavItem.startsWith('/company') }" @click="handleToPath('/company')">
公司列表
<div class="underline"></div>
</div>
<div class="nav-item" :class="{ active: activeNavItem.startsWith('/forum') }" @click="handleToPath('/forum')">
论坛中心
<div class="underline"></div>
</div>
</div>
</div>
<div class="right">
<div class="actions">
<div class="action-item" :class="{ active: activeNavItem.startsWith('/messages') }">
<Messages v-if="userStore.isLogined" />
<div class="underline"></div>
</div>
<div class="action-item" :class="{ active: activeNavItem.startsWith('/settings') }">
<User />
<div class="underline"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from "@/store/user";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import Messages from "./components/messages.vue";
import User from "./components/user.vue";
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const activeNavItem = ref("");
watch(
() => route.path,
(newVal) => {
activeNavItem.value = newVal;
},
{ immediate: true, deep: true }
);
const isAtTop = ref(true);
const handleScroll = () => {
const scrollTop = window.scrollY; // 获取页面滚动的高度
isAtTop.value = scrollTop === 0; // 如果滚动到顶部,isAtTop 为 true
};
onMounted(() => {
window.addEventListener("scroll", handleScroll); // 添加滚动事件监听
});
onBeforeUnmount(() => {
window.removeEventListener("scroll", handleScroll); // 移除滚动事件监听
});
function handleToPath(path: string) {
router.push(path);
}
</script>
<style lang="scss" scoped>
.topbar {
position: fixed;
top: 0px;
z-index: 999;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 50px;
background-color: #ffffff;
box-shadow: rgba(0, 0, 0, 0.04) 0px 2px 8px -2px;
padding: 0px 200px;
}
.at-top {
background-color: #ffffff;
box-shadow: none;
}
.left {
height: 100%;
width: 120px;
}
.center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
gap: 24px;
height: 100%;
}
.right {
display: flex;
gap: 12px;
justify-content: flex-end;
height: 100%;
}
.logo {
height: 100%;
width: 160px;
cursor: pointer;
}
.nav {
display: flex;
gap: 12px;
height: 100%;
width: 100%;
justify-content: center;
align-items: center;
}
.nav-item {
font-size: 14px;
font-weight: bold;
height: 100%;
margin: 0 12px;
display: flex;
align-items: center;
color: #000;
cursor: pointer;
line-height: 50px;
}
.actions {
display: flex;
gap: 24px;
height: 100%;
width: 100%;
align-items: flex-end;
font-size: 14px;
.action-item {
height: 100%;
font-size: 14px;
display: flex;
align-items: center;
cursor: pointer;
}
}
.action-item,
.nav-item {
position: relative;
.underline {
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background-color: #007bff;
transform: translateX(-50%) scaleX(0);
transform-origin: center;
transition: transform 0.3s ease; // 仅在需要动画时应用过渡效果
}
&.active {
color: #007bff;
}
&.active .underline,
&:hover .underline {
width: 100%;
transform: translateX(-50%) scaleX(1);
}
// 默认状态下不应用过渡效果
&:not(.active):not(:hover) .underline {
transition: none;
}
}
</style>