starwait/pages/home/blog.vue
2025-04-29 17:01:17 +08:00

316 lines
9.2 KiB
Vue

<script setup lang="ts">
import rfdc from "rfdc";
import type {blogMenu} from "~/drizzle/schema";
import {useAsyncData} from "nuxt/app";
const clone = rfdc()
const niMessage = useNiMessage()
const {data: obm, error} = await useFetch('/api/blog/blogMenu');
if (error.value) {
throw error.value;
}
definePageMeta({
layout: 'home',
pageTransition: {
name: 'slide',
mode: 'out-in',
},
})
// 菜单数据类型
type BlogMenu = typeof blogMenu.$inferInsert;
type ShowBlogMenu = BlogMenu & {
rank?: number;
opne?: boolean;
openNumber?: number;
childrenLength?: number;
}
// 菜单原始数据
const originalBlogMenuList = shallowRef<ShowBlogMenu[]>(obm as ShowBlogMenu)
// 根据pid存储列表Map
const blogMenuPidMap = ref(new Map)
// 根据id存储对象Map
const blogMenuIdMap = ref(new Map)
// blog渲染列表
const blogMenuList = ref<ShowBlogMenu[]>([]);
// blog菜单对象
const blogMenuObject = ref<ShowBlogMenu>(null);
// 选中的blog菜单对象
const activeBlogMenuObject = ref<ShowBlogMenu>(null);
// 菜单折叠
const menuCollapseStatus = ref(true)
// blog信息弹窗状态
const blogInfoPopupStatus = ref(false)
// 新增的blog菜单Pid
const insertBlogMenuPid = ref<string>('');
// ---- 数据相关
// 格式化菜单数据
const formatBlogMenuListToPidMap = (blogMenuList: Array<ShowBlogMenu>) => {
const idMap = new Map()
const pidMap = new Map()
const list = clone(blogMenuList)
const pidObj: Record<string, ShowBlogMenu[]> = {};
for(const menuItem of list){
if(pidObj[menuItem.pid]){
pidObj[menuItem.pid].push(menuItem)
}else{
pidObj[menuItem.pid] = [menuItem]
}
if(menuItem.pid == 0){
menuItem.rank = 0;
}
menuItem.openNumber = 0;
}
for(const menuItem of list){
const id = menuItem.id;
if(pidObj[id]){
menuItem.childrenLength = pidObj[id].length
}
idMap.set(id, menuItem)
}
Object.keys(pidObj).forEach(key => {
pidMap.set(key, pidObj[key]);
})
blogMenuPidMap.value = pidMap
blogMenuIdMap.value = idMap
}
// 初始化blog菜单数据
const initMenuData = () => {
const blogMenuData = originalBlogMenuList.value
formatBlogMenuListToPidMap(blogMenuData);
blogMenuList.value = blogMenuPidMap.value.get('0')
}
// 客户端获取菜单数据
const getMenuListForClient = async () => {
const resd = await $fetch('/api/blog/blogMenu').catch(e => {
consola.warn('获取博客目录异常', e)
niMessage.warning('获取博客目录异常!')
});
if(resd){
originalBlogMenuList.value = resd;
initMenuData()
}
}
// 修改上级菜单的打开数量
const changeParentOpenNumber = (item: ShowBlogMenu, openNumber: number, type = '-') => {
if(item.pid === '0'){
item.openNumber = openNumber
return
}
let targetMenuItem = item;
while(true){
// 获取父元素
targetMenuItem = blogMenuIdMap.value.get(targetMenuItem.pid);
if(type == '-'){
targetMenuItem.openNumber -= openNumber
}else{
targetMenuItem.openNumber += openNumber
}
if(targetMenuItem.pid === '0'){
break;
}
}
if(type == '-'){
item.openNumber = 0
}else{
item.openNumber = openNumber
}
}
// --- 事件
// 展开/关闭子菜单事件
const handleCollapseChildrenEvent = (item: ShowBlogMenu, index: number) => {
if(item.open == true){
// 闭合
// 将状态关闭
blogMenuIdMap.value.get(item.id).open = false;
// 删除子孙元素
blogMenuList.value.splice(index + 1, item.openNumber);
// 降低父辈元素的打开数量
changeParentOpenNumber(item, item.openNumber, '-');
//
}else{
// 展开
// 将状态打开
blogMenuIdMap.value.get(item.id).open = true;
// 获取子元素
const childrenList = blogMenuPidMap.value.get(item.id)
blogMenuList.value.splice(index + 1, 0, ...childrenList.map(i => {
i.rank = item.rank + 1;
i.open = false;
return i
}));
changeParentOpenNumber(item, childrenList.length, '+');
}
}
// 新增blog事件
const handleCreateBlogMenuItemEvent = (pid: string) => {
blogMenuObject.value = {
pid,
name: '',
desc: ''
}
blogInfoPopupStatus.value = true;
insertBlogMenuPid.value = pid
}
// 确认新增
const handleCreateBlogMenuItemAck = async () => {
const resd= await $fetch('/api/blog/blogMenu',{
method: 'POST',
body: blogMenuObject.value
}).catch(e => {
consola.error(e);
niMessage.warning('创建博客失败!');
});
if(resd){
niMessage.success('创建博客成功!');
blogInfoPopupStatus.value = false;
const father = blogMenuIdMap.value.get(insertBlogMenuPid.value);
resd.opne = false;
resd.openNumber = 0;
resd.childrenLength = 0;
blogMenuIdMap.value.set(resd.id, resd)
let pidMap
if(insertBlogMenuPid.value == '0'){
resd.rank = 0;
pidMap = blogMenuPidMap.value.get('0')
}else{
resd.rank = father.rank + 1;
father.childrenLength = father.childrenLength + 1;
pidMap = blogMenuPidMap.value.get(father.id)
}
if(pidMap){
pidMap.push(resd)
}else{
blogMenuPidMap.value.set(father.id, [resd])
}
if(father?.open){
const openNumber = father.openNumber
changeParentOpenNumber(father, 1, '+');
const insertIndex = blogMenuList.value.findIndex(i => i.id === father.id)
father.openNumber = father.openNumber + openNumber
blogMenuList.value.splice(insertIndex + father.openNumber, 0, resd);
}else if(insertBlogMenuPid.value == '0'){
blogMenuList.value = blogMenuPidMap.value.get('0')
}
}
}
// SSR运行
initMenuData()
onMounted(() => {
})
</script>
<template>
<div class="homeBlog">
<HomeBlogMenu
class="left"
v-model:status="menuCollapseStatus"
v-model:active="activeBlogMenuObject"
:blogMenuList
@createBlogMenu="handleCreateBlogMenuItemEvent"
@collapseChildren="handleCollapseChildrenEvent"></HomeBlogMenu>
<div class="main">
<header>
<div class="menuCollapse a11 allCenter point" :class="menuCollapseStatus && 'hide'">
<div class="star-blogIconFont" @click="menuCollapseStatus = !menuCollapseStatus">&#xe67f;</div>
</div>
</header>
<main>
<NuxtPage />
</main>
</div>
<div class="right"></div>
<NiPopup v-model:status="blogInfoPopupStatus">
<template #header>博客菜单</template>
<NiForm :data="blogMenuObject">
<NiFormItem label="博客名称" name="name" label-width="6">
<NiInput v-model="blogMenuObject.name"></NiInput>
</NiFormItem>
<NiFormItem label="博客描述" name="name" label-width="6">
<NiInput v-model="blogMenuObject.desc"></NiInput>
</NiFormItem>
<div>{{ blogMenuObject.name }}</div>
<div>{{ blogMenuObject.desc }}</div>
</NiForm>
<template #footer>
<NiButton @click="blogInfoPopupStatus = false">取消</NiButton>
<NiButton type="primary" @click="handleCreateBlogMenuItemAck">确认</NiButton>
</template>
</NiPopup>
</div>
</template>
<style scoped lang="scss">
.homeBlog {
position: relative;
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
& > div.left{
position: relative;
height: 100%;
flex-shrink: 0;
background: var(--blog-menu-background-color);
border-right: 1px solid var(--bg-color-be);
}
& > div.right{
position: relative;
height: 100%;
overflow: auto;
flex-shrink: 0;
}
& > div.main{
position: relative;
height: 100%;
overflow: auto;
flex: 1;
display: flex;
flex-direction: column;
& > header{
position: relative;
flex-shrink: 0;
display: flex;
height: 3rem;
border-bottom: 1px solid var(--bg-color-be);
& > div.menuCollapse{
position: relative;
height: 100%;
transition: width .3s;
width: 3rem;
overflow: hidden;
&:hover & > div{
color: var(--font-color-top3-h1);
}
&.hide{
width: 0;
}
& > div{
font-size: 1.3rem;
color: var(--font-color-top3-h2);
transition: color .5s, opacity .5s;
}
}
}
& > main{
position: relative;
flex: 1;
overflow: hidden;
}
}
}
@media (max-width: 767px){
}
@media (min-width: 768px){
}
</style>