You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

836 lines
24 KiB

<template>
<Simplebar @scroll="handleScroll">
<!-- 头部导航菜单 -->
<div class="headerMenu" v-if="show_menu">
<a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" style="border-bottom: none;"
@click="jumpMenu" />
<a-input-search v-model:value="searchValue" placeholder="search" style="width: 200px" @search="onSearch" />
</div>
<div class="author" v-if="show_author">{{ author }}</div>
<!-- 轮播 -->
<div class="carousel" v-if="show_carousel">
<a-carousel autoplay>
<div class="img"><img src="/src/assets/images/nav1.png" alt=""></div>
<div class="img"><img src="/src/assets/images/nav10.png" alt=""></div>
<div class="img"><img src="/src/assets/images/nav13.png" alt=""></div>
</a-carousel>
</div>
<!-- 操作按钮滚动 -->
<div class="anchorDown" v-if="show_anchornDown">
<a-button type="link" shape="circle" size="large" @click="downScroll">
<template #icon>
<DownCircleOutlined spin />
</template>
</a-button>
</div>
<!-- 主要内容区域 -->
<div class="mainContainer" :style="mainCss">
<div class="leftBar">
<a-card hoverable>
<template #cover>
<img alt="example" src="/src/assets/images/头像.jpg" />
</template>
<h1>sunfree</h1>
<div class="cardText"></div>
<div class="button-group">
<a-button v-for="(button, index) in buttons" :key="index" shape="circle" size="large"
@click="handleClick(button.url)">
<component :is="button.icon" />
</a-button>
</div>
<div class="fold-panel">
<a-collapse v-model:activeKey="activeKey" :bordered="false" expandIconPosition="end">
<template #expandIcon="{ isActive }">
<caret-right-outlined :rotate="isActive ? 90 : 0" />
</template>
<a-collapse-panel v-for="classtic in classticlist" :key=classtic.id :header=classtic.header
:style="customStyle">
<p>{{ classtic.text }}</p>
</a-collapse-panel>
</a-collapse>
</div>
</a-card>
<a-card hoverable title="常用链接">
<div class="button-group">
<a-button type="dashed" v-for="comlink in comlinklist" :key="comlink.id"
@click="comLinkClick(comlink.linkurl)">{{
comlink.linktext
}}</a-button>
</div>
</a-card>
</div>
<RouterView />
<div class="rightBar">
<a-card hoverable>
<div class="statistic">
<a-row>
<a-col :span="8" v-for="statistic in statistics" :key="statistic.id">
<a-statistic :title=statistic.title :value=statistic.counts />
</a-col>
</a-row>
</div>
</a-card>
<a-card hoverable>
<template #cover>
<div class="heatmap" style="border-right: rgba(0, 0, 0, 0.5);">
<div ref="heat" style="height: 100%;"></div>
</div>
</template>
</a-card>
<a-card hoverable>
<template #cover>
<div id="aplayer"></div>
</template>
</a-card>
<a-card title="随机文章" :bordered="false" hoverable>
<div v-for="article in 3">
<a-image :preview="false" :width="300"
src="https://cdn.naccl.top/blog/blogHosting/2024/02/B01/f56c5bbe-469c-4eb7-a994-9281d6eed689.png" />
<div class="article-text">
<span>2021-02-01</span>
<span>操作系统</span>
</div>
</div>
</a-card>
<a-card title="标签云" :bordered="false" hoverable>
sces
</a-card>
</div>
</div>
<div class="footer">
<div class="miit">
<a-space wrap>
<a-text type="link">
<component :is=iconComponents.BanQuanLined />
<span class="miit-style">Copyright © 2024</span>
</a-text>
<a-button type="link" href="/home">
<component :is=iconComponents.TitleOutLined />
<span>SunFree</span>
</a-button>
<a-button type="link" href="https://beian.miit.gov.cn/" target="_blank">
<component :is=iconComponents.IcpLined />
<span>苏ICP备2024067473号-1</span>
</a-button>
<a-button type="link" href="https://beian.mps.gov.cn/#/query/webSearch" target="_blank">
<component :is=iconComponents.GongAnLined />
<span>苏公网安备32021402003003号</span>
</a-button>
</a-space>
</div>
<div class="badge">
<a-space wrap>
<a-button type="link" class="badge-button" href="https://cn.vuejs.org/" target="_blank">
<span class="tag-nav">front-end</span>
<span class="tag-main-first">Vue.js</span>
</a-button>
<a-button type="link" class="badge-button" href="https://fastapi.tiangolo.com/zh/" target="_blank">
<span class="tag-nav">back-end</span>
<span class="tag-main-second">FastAPI</span>
</a-button>
<a-button type="link" class="badge-button" href="https://www.antdv.com/docs/vue/introduce-cn/"
target="_blank">
<span class="tag-nav">UI</span>
<span class="tag-main-third">Ant Design Vue</span>
</a-button>
<a-button type="link" class="badge-button" href="https://cloud.tencent.com/" target="_blank">
<span class="tag-nav">VPS</span>
<span class="tag-main-fourth">tencent cloud</span>
</a-button>
</a-space>
</div>
<div>
</div>
<div></div>
</div>
</Simplebar>
</template>
<script lang="ts" setup>
import { h, reactive, ref, nextTick, toRefs, computed } from 'vue';
import type { MenuProps } from 'ant-design-vue';
import { HomeOutlined, HighlightOutlined, ProfileOutlined, CameraOutlined, UsergroupDeleteOutlined, DownCircleOutlined } from '@ant-design/icons-vue';
import Typed from 'typed.js';
import { onMounted, watch } from 'vue';
import { CaretRightOutlined } from '@ant-design/icons-vue';
import 'APlayer/dist/APlayer.min.css';
import APlayer from 'APlayer';
import { get } from "@/tools/request"
import { createEcharts } from "@/hooks/intex"
import { useRouter, useRoute } from 'vue-router';
import iconComponents from "@/assets/index";
import type { classticInterface, comLinkInterface } from '@/api/admin';
const router = useRouter()
const route = useRoute()
const author = ref("SunFree.")
/**
* 隐藏参数
*/
const idShow = reactive({
show_menu: false,
show_carousel: true,
show_author: true,
show_anchornDown: true
})
const { show_menu, show_carousel, show_author, show_anchornDown } = toRefs(idShow)
const mainCss = reactive({
marginTop: "0px"
})
const current = ref<string[]>(['home']);
const items = ref<MenuProps['items']>([
{
key: 'home',
icon: () => h(HomeOutlined),
label: '首页',
title: '首页',
},
{
key: 'blog',
icon: () => h(HighlightOutlined),
label: '博客',
title: '博客',
},
{
key: 'diary',
icon: () => h(ProfileOutlined),
label: '日记',
title: '日记',
},
{
key: 'album',
icon: () => h(CameraOutlined),
label: '相册',
title: '相册',
children: [
{
label: '相册1',
key: 'album1',
},
{
label: '相册2',
key: 'album2',
},
{
label: '相册3',
key: 'album3',
},
{
label: '相册4',
key: 'album4',
},
],
},
{
key: 'chart',
icon: () => h(UsergroupDeleteOutlined),
label: '收支图',
title: '收支图',
},
{
key: 'aboutme',
icon: () => h(UsergroupDeleteOutlined),
label: '关于sunfree',
title: '关于sunfree',
},
]);
const searchValue = ref("")
const jumpMenu = ({ key }: { key: string }) => {
router.push(key)
};
const updateCarouselVisibility = (routeName: any) => {
handleScrollEnabled.value = false;
if (scrollbar.value) {
scrollbar.value.scrollTop = 0;
}
if (routeName === 'home') {
handleScrollEnabled.value = true;
show_carousel.value = true;
show_menu.value = false
} else {
show_menu.value = true;
show_author.value = false;
show_carousel.value = false;
show_anchornDown.value = false;
mainCss.marginTop = '48px';
}
};
router.beforeEach((to, _, next) => {
updateCarouselVisibility(to.name);
next();
});
// 查询功能
const articleTitle = ref<string>('');
const onSearch = (searchValue: string) => {
console.log('use value', searchValue);
console.log('or use this.value', articleTitle.value);
};
/**
* 滚动条操作
*/
const scrollbar = ref<Element | null>(null);
const handleScrollEnabled = ref(true);
// 定义滚动条滚到一半显示导航菜单
const handleScroll = () => {
if (!handleScrollEnabled.value) return;
if (scrollbar.value) {
const scrollOffset = scrollbar.value.scrollTop;
const halfViewportHeight = scrollbar.value.clientHeight / 2;
show_menu.value = scrollOffset > halfViewportHeight;
}
};
// 点击按钮,实现滚动到视窗高度距离
const downScroll = () => {
if (scrollbar.value) {
const scrollTop = scrollbar.value.scrollTop;
const viewportHeight = scrollbar.value.clientHeight;
const scrollDistance = viewportHeight - scrollTop - 48;
setTimeout(() => {
scrollbar.value!.scrollBy({ top: scrollDistance, behavior: 'smooth' });
}, 300);
}
};
/**
* 左侧栏
*/
// 折叠面板
const classticlist = ref<classticInterface[]>([])
const classticList = async () => {
try {
await get("/classtics/list").then(response => {
if (response) {
classticlist.value = response.data.data.map((item: any, index: any) => ({
key: (index + 1).toString(),
header: item.header,
text: item.text,
descr: item.descr
}));
} else {
console.log("the interface request data does not exist!")
}
})
} catch (error) {
console.error("Failed to fetch data", error);
}
}
const comLinkClick = (url: string) => {
if (url.startsWith('http://') || url.startsWith('https://')) {
window.open(url, "_blank"); // 使用 window.location.href 打开外部链接
} else {
router.push(url); // 否则使用 Vue Router 的 push 方法导航
}
}
const comlinklist = ref<comLinkInterface[]>([])
const comLinkList = async () => {
try {
await get("/comlinks/list").then(response => {
if (response) {
comlinklist.value = response.data.data.map((item: any, index: any) => ({
key: (index + 1).toString(),
id: item.id,
linktext: item.linktext,
linkurl: item.linkurl,
descr: item.descr
}));
} else {
console.log("the interface request data does not exist!")
}
})
} catch (error) {
console.error("Failed to fetch data", error);
}
}
const activeKey = ref(['']);
const customStyle = 'background: #F5F5F5;border-radius: 4px;margin-bottom: 12px;border: 0;overflow: hidden';
const buttons = ref([
{ icon: iconComponents.CravatarLined, url: 'https://cravatar.cn/' },
{ icon: iconComponents.QQLined, url: '/qqcode' },
{ icon: iconComponents.WechatLined, url: '/wechatcode' },
{ icon: iconComponents.MusicLined, url: 'https://music.163.com/#/playlist?id=160266689' },
{ icon: iconComponents.GitHubLined, url: 'https://gitee.com/c_panda' },
]);
const handleClick = (url: string) => {
window.open(url)
}
/**
* 右侧栏
*/
// 统计
const createData = new Date('2024-07-12');
const currentDate = ref(new Date());
const getDateOnly = (date: Date) => {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
};
// 计算相差的天数
const daysDifference = computed(() => {
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
const diffTime = Math.abs(getDateOnly(currentDate.value).getTime() - getDateOnly(createData).getTime());
return Math.floor(diffTime / oneDay); // 使用Math.floor确保是整天数
});
const diaryTotal = ref();
const blogTotal = ref();
const diaryList = async () => {
try {
const response = await get('/diarys/list');
diaryTotal.value = response.data.data.total; // 更新总数
} catch (error) {
console.error('Failed to fetch data', error);
}
};
const blogList = async () => {
try {
const response = await get("/blogs/list");
if (response) {
blogTotal.value = response.data.data.total;
} else {
console.log("bloglist is not exits")
}
} catch (error) {
console.log("bloglist is error")
}
}
// 更新当前时间
const updateCurrentDate = () => {
currentDate.value = new Date();
};
const statistics = reactive([
{
id: "1",
title: "DAYS",
counts: daysDifference
},
{
id: "2",
title: "DIARYS",
counts: diaryTotal
},
{
id: "3",
title: "BLOGS",
counts: blogTotal
},
])
// 日历热力图
const heat = ref(null);
function generateDates(numDays: number) {
const dates = [];
const currentDate = new Date();
for (let i = 0; i < numDays; i++) {
const date = new Date(currentDate);
date.setDate(currentDate.getDate() - i);
dates.push(
{ date: date.toISOString().split('T')[0], writCount: 0 }
); // 包含日期和评论数
}
return dates;
}
// 初始化60天的数据
const data = generateDates(60);
// 重新排列数据
const rawData = ref<any[]>([]);
const statisticList = async () => {
await get("/statistics/list").then(response => {
rawData.value = response.data.data
rawData.value.forEach(newDataItem => {
const item = newData.find((d: any) => d.date === newDataItem.date);
if (item) {
item.writCount = newDataItem.writCount;
}
});
let formattedData = newData.map((item: any, index: number) => {
return [index % 15, Math.floor(index / 15), item.writCount]
});
console.log(formattedData, "form");
formattedList(formattedData)
createEcharts(heat, heatMapData);
})
}
const formattedList = (val: any) => {
heatMapData.series[0].data = val
}
const newData = <any>[];
for (let i = 0; i < 60; i += 15) {
// 取出每个15天的数据,并反转顺序
const chunk = data.slice(i, i + 15).reverse();
newData.push(...chunk);
}
const heatMapData = reactive(
{
tooltip: {
position: 'top',
formatter: function (params: any) {
const item = newData[params.dataIndex];
if (item.writCount > 0) {
return `${item.date}<br/>COMMENTS: ${item.writCount}`;
} else {
return `${item.date}`;
}
}
},
grid: {
left: '0', // 左边距
right: '0', // 右边距
top: '0', // 上边距
bottom: '0',
},
xAxis: {
type: 'category',
data: Array.from({ length: 15 }, (_, i) => `Col ${i + 1}`),
splitArea: {
show: true
},
show: false
},
yAxis: {
type: 'category',
data: ['Row 1', 'Row 2', 'Row 3', 'Row 4'],
splitArea: {
show: false
},
show: false
},
visualMap: {
min: 0,
max: 20,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '15%',
show: false,
inRange: {
color: ['rgba(182,181,178,0.01)', 'rgba(157,156,153,1)'] // 0评论是白色, 非0评论是黑色
}
},
series: [{
type: 'heatmap',
data: [],
itemStyle: {
borderColor: 'rgb(231,229,225,0.5)', // 设置边框颜色
borderWidth: 0.5, // 设置边框宽度
},
label: {
show: false,
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)',
borderColor: '#fff', // 鼠标悬停时的边框颜色
borderWidth: 2 // 鼠标悬停时的边框宽度
}
}
}]
}
)
// 音乐组件
const random = ref();
onMounted(() => {
classticList()
blogList()
diaryList()
comLinkList()
statisticList()
updateCurrentDate();
setInterval(updateCurrentDate, 24 * 60 * 60 * 1000);
createEcharts(heat, heatMapData);
nextTick(() => {
const authorElement = document.querySelector('.author');
if (authorElement) {
new Typed('.author', {
strings: ['SunFree.'],
typeSpeed: 200,
backSpeed: 150,
loop: true,
loopCount: Infinity,
showCursor: false
});
};
const cardTextElement = document.querySelector('.cardText');
if (cardTextElement) {
// 定义个人座右铭
new Typed('.cardText', {
strings: ['做三流测试,品瀚霖人生!'],
typeSpeed: 200,
backSpeed: 150,
loop: true, // 开启循环
loopCount: Infinity, // 无限循环
showCursor: false // 取消光标
});
};
const aplayerContainer = document.getElementById('aplayer');
if (aplayerContainer) {
new APlayer({
container: aplayerContainer,
mini: false,
autoplay: false,
theme: '#FADFA3',
loop: 'all',
order: 'random',
preload: 'auto',
volume: 0.7,
mutex: true,
listFolded: true,
listMaxHeight: 90,
lrcType: 3,
audio: [
// {
// name: 'name1',
// artist: 'artist1',
// url: 'url1.mp3',
// cover: 'cover1.jpg',
// lrc: 'lrc1.lrc',
// theme: '#ebd0c2'
// },
// {
// name: 'name2',
// artist: 'artist2',
// url: 'url2.mp3',
// cover: 'cover2.jpg',
// lrc: 'lrc2.lrc',
// theme: '#46718b'
// }
]
});
}
});
scrollbar.value = document.querySelector('.simplebar-content-wrapper');
})
watch(
() => route.name,
(newRouteName) => {
updateCarouselVisibility(newRouteName);
if (newRouteName) {
const menuItem = items.value?.find((item: any) => item.key === newRouteName);
if (menuItem) {
current.value = [menuItem.key as string];
} else {
current.value = ['home']; // 如果找不到对应的菜单项,默认选中 'home'
}
} else {
current.value = ['home']; // 如果 route.name 不存在,默认选中 'home'
}
},
{ immediate: true }
);
</script>
<style scoped>
.headerMenu {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 48px;
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
position: fixed;
background-color: white;
transform: translateX(-50%);
top: 0;
left: 50%;
z-index: 999;
}
/* 作者名称 */
.author {
position: absolute;
display: inline-block;
font-size: 100px;
font-family: 'Courier New', Courier, monospace;
background: linear-gradient(270deg, #d9d8d6, #7d7771, rgb(85, 68, 68));
background-size: 400% 400%;
-webkit-background-clip: text;
color: transparent;
background-clip: text;
animation: salon-light-animation 3s ease infinite;
top: 50%;
left: 50%;
transform: translateX(-50%);
z-index: 999;
}
@keyframes salon-light-animation {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.carousel img {
height: 100vh;
width: 100vw;
}
.anchorDown {
position: absolute;
bottom: 100px;
left: 50%;
border-style: hidden;
transform: translateX(-50%);
}
.anchorDown span {
font-size: 35px;
color: aliceblue;
}
.mainContainer {
display: flex;
justify-content: center;
padding-top: 48px;
background-color: rgba(5, 5, 5, 0.08);
}
.leftBar {
width: 15%;
}
.leftBar img {
/* 图片自适应容器并保持宽高比例 */
aspect-ratio: 1/1;
}
.leftBar>* {
margin-bottom: 24px;
}
.leftBar h1 {
text-align: center;
font-family: Georgia, 'Times New Roman', Times, serif;
}
.leftBar .cardText {
min-height: 60px;
text-align: center;
}
.leftBar>:first-child .button-group {
display: flex;
margin: 0 12px 24px 12px;
justify-content: space-between;
}
.leftBar>:nth-child(2) .button-group {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.leftBar>:nth-child(2) .button-group>* {
width: 40%;
margin: 12px;
}
.rightBar {
width: 15%;
}
.rightBar>* {
margin-bottom: 24px;
}
.rightBar>:nth-child(4) div {
display: flex;
justify-content: center;
position: relative;
margin-bottom: 24px;
}
.rightBar>:nth-child(4) div .article-text {
display: flex;
flex-direction: column;
position: absolute;
bottom: 5%;
left: 10%;
color: rgb(187, 185, 187);
}
#aplayer {
margin: 0;
}
.statistic {
text-align: center;
}
.footer {
display: flex;
flex-direction: column;
align-items: center;
}
.footer a {
color: #6e5555;
}
.footer .miit {
margin: 48px 0 0 0;
}
.footer .badge {
margin: 48px 0;
}
.footer .badge .tag-nav {
background-color: #505050;
}
.footer .miit-style {
padding: 0 0 0 6px;
}
.footer .badge span {
padding: 0 6px;
color: white;
}
.footer .badge .tag-main-first {
background-color: rgb(3, 3, 250);
}
.footer .badge .tag-main-second {
background-color: rgb(235, 15, 228);
}
.footer .badge .tag-main-third {
background-color: rgb(10, 207, 39);
}
.footer .badge .tag-main-fourth {
background-color: rgb(229, 131, 12);
}
</style>