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.

673 lines
19 KiB

10 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
10 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
10 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
9 months ago
10 months ago
  1. <template>
  2. <Simplebar @scroll="handleScroll">
  3. <!-- 头部导航菜单 -->
  4. <div class="headerMenu" v-if="show_menu">
  5. <a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" style="border-bottom: none;" />
  6. <a-input-search v-model:value="value" placeholder="search" style="width: 200px" @search="onSearch" />
  7. </div>
  8. <div class="author" v-if="show_author"></div>
  9. <!-- 轮播 -->
  10. <div class="carousel" v-if="show_carousel">
  11. <a-carousel autoplay>
  12. <div class="img"><img src="/src/assets/images/nav1.png" alt=""></div>
  13. <div class="img"><img src="/src/assets/images/nav10.png" alt=""></div>
  14. <div class="img"><img src="/src/assets/images/nav13.png" alt=""></div>
  15. </a-carousel>
  16. </div>
  17. <!-- 操作按钮滚动 -->
  18. <div class="anchorDown" v-if="show_anchornDown">
  19. <a-button type="link" shape="circle" size="large" @click="downScroll">
  20. <template #icon>
  21. <DownCircleOutlined style="font-size: 35px;color: aliceblue;" spin />
  22. </template>
  23. </a-button>
  24. </div>
  25. <!-- 主要内容区域 -->
  26. <div class="mainContainer" :style="mainCss">
  27. <div class="leftBar">
  28. <a-card hoverable>
  29. <template #cover>
  30. <img alt="example" src="/src/assets/images/头像.jpg" />
  31. </template>
  32. <h1>sunfree</h1>
  33. <div class="cardText"></div>
  34. <div class="button-group">
  35. <a-button v-for="(button, index) in buttons" :key="index" shape="circle" size="large"
  36. @click="handleClick(button.url)">
  37. <component :is="button.icon" />
  38. </a-button>
  39. </div>
  40. <div class="fold-panel">
  41. <a-collapse v-model:activeKey="activeKey" :bordered="false" expandIconPosition="end">
  42. <template #expandIcon="{ isActive }">
  43. <caret-right-outlined :rotate="isActive ? 90 : 0" />
  44. </template>
  45. <a-collapse-panel v-for="panel in panels" :key=panel.id :header=panel.header
  46. :style="customStyle">
  47. <p>{{ panel.text }}</p>
  48. </a-collapse-panel>
  49. </a-collapse>
  50. </div>
  51. </a-card>
  52. <a-card hoverable title="常用链接">
  53. <div class="button-group">
  54. <a-button type="dashed" v-for="linkbutton in linkbuttons" :key="linkbutton.id"
  55. @click="comLinkClick(linkbutton.linkurl)">{{
  56. linkbutton.linktext
  57. }}</a-button>
  58. </div>
  59. </a-card>
  60. </div>
  61. <RouterView />
  62. <div class="rightBar">
  63. <a-card hoverable>
  64. <div class="statistic">
  65. <a-row>
  66. <a-col :span="8" v-for="statistic in statistics" :key="statistic.id">
  67. <a-statistic :title=statistic.title :value=statistic.counts />
  68. </a-col>
  69. </a-row>
  70. </div>
  71. </a-card>
  72. <a-card hoverable>
  73. <template #cover>
  74. <div class="heatmap" style="border-right: rgba(0, 0, 0, 0.5);">
  75. <div ref="heat" style="height: 100%;"></div>
  76. </div>
  77. </template>
  78. </a-card>
  79. <a-card hoverable>
  80. <template #cover>
  81. <div id="aplayer"></div>
  82. </template>
  83. </a-card>
  84. <a-card title="随机文章" :bordered="false" hoverable>
  85. <div v-for="article in 3">
  86. <a-image :preview="false" :width="300"
  87. src="https://cdn.naccl.top/blog/blogHosting/2024/02/B01/f56c5bbe-469c-4eb7-a994-9281d6eed689.png" />
  88. <div class="article-text">
  89. <span>2021-02-01</span>
  90. <span>操作系统</span>
  91. </div>
  92. </div>
  93. </a-card>
  94. <a-card title="标签云" :bordered="false" hoverable>
  95. sces
  96. </a-card>
  97. </div>
  98. </div>
  99. </Simplebar>
  100. </template>
  101. <script lang="ts" setup>
  102. import { h, reactive, ref, nextTick } from 'vue';
  103. import type { MenuProps } from 'ant-design-vue';
  104. import { HomeOutlined, HighlightOutlined, ProfileOutlined, CameraOutlined, UsergroupDeleteOutlined, DownCircleOutlined } from '@ant-design/icons-vue';
  105. import Typed from 'typed.js';
  106. import { onMounted, watch } from 'vue';
  107. import type { CSSProperties } from 'vue';
  108. import { CaretRightOutlined } from '@ant-design/icons-vue';
  109. import 'APlayer/dist/APlayer.min.css';
  110. import APlayer from 'APlayer';
  111. import { createEcharts } from "@/hooks/intex"
  112. import { useRouter, useRoute } from 'vue-router';
  113. import iconComponents from "@/assets/index";
  114. import { get } from "@/tools/request";
  115. import { number } from 'echarts';
  116. const router = useRouter()
  117. const route = useRoute()
  118. /**
  119. * 导航菜单
  120. */
  121. const show_menu = ref(false);
  122. const show_carousel = ref(true);
  123. const show_author = ref(true)
  124. const show_anchornDown = ref(true)
  125. const mainCss = reactive({
  126. marginTop: "0px"
  127. })
  128. type MenuProps = any;
  129. const current = ref<string[]>(['home']);
  130. const items = ref<MenuProps['items']>([
  131. {
  132. key: 'home',
  133. icon: () => h(HomeOutlined),
  134. label: h('a', { href: '/' }, '首页'),
  135. title: '首页',
  136. },
  137. {
  138. key: 'blog',
  139. icon: () => h(HighlightOutlined),
  140. label: h('a', { href: "/blog" }, '博客'),
  141. title: '博客',
  142. },
  143. {
  144. key: 'diary',
  145. icon: () => h(ProfileOutlined),
  146. label: h('a', { href: "/diary" }, '日记'),
  147. title: '日记',
  148. },
  149. {
  150. key: 'album',
  151. icon: () => h(CameraOutlined),
  152. label: '相册',
  153. title: '相册',
  154. children: [
  155. {
  156. label: '相册1',
  157. key: 'album1',
  158. // url: "album1"
  159. },
  160. {
  161. label: '相册2',
  162. key: 'album2',
  163. },
  164. {
  165. label: '相册3',
  166. key: 'album3',
  167. },
  168. {
  169. label: '相册4',
  170. key: 'album4',
  171. },
  172. ],
  173. },
  174. {
  175. key: 'chart',
  176. icon: () => h(UsergroupDeleteOutlined),
  177. label: h('a', { href: "/chart" }, '收支图'),
  178. title: '收支图',
  179. },
  180. {
  181. key: 'aboutme',
  182. icon: () => h(UsergroupDeleteOutlined),
  183. label: h('a', { href: "/aboutme" }, '关于sunfree'),
  184. title: '关于sunfree',
  185. },
  186. ]);
  187. const updateCarouselVisibility = (routeName: any) => {
  188. handleScrollEnabled.value = false;
  189. if (scrollbar.value) {
  190. scrollbar.value.scrollTop = 0;
  191. }
  192. if (routeName === 'home') {
  193. handleScrollEnabled.value = true;
  194. show_carousel.value = true;
  195. show_menu.value = false
  196. } else {
  197. show_menu.value = true;
  198. show_author.value = false;
  199. show_carousel.value = false;
  200. show_anchornDown.value = false;
  201. mainCss.marginTop = '48px';
  202. }
  203. };
  204. router.beforeEach((to, from, next) => {
  205. updateCarouselVisibility(to.name);
  206. next();
  207. });
  208. // 查询功能
  209. const articleTitle = ref<string>('');
  210. const onSearch = (searchValue: string) => {
  211. console.log('use value', searchValue);
  212. console.log('or use this.value', articleTitle.value);
  213. };
  214. /**
  215. * 滚动条操作
  216. */
  217. const scrollbar = ref<Element | null>(null);
  218. const handleScrollEnabled = ref(true);
  219. // 定义滚动条滚到一半显示导航菜单
  220. const handleScroll = () => {
  221. if (!handleScrollEnabled.value) return;
  222. if (scrollbar.value) {
  223. const scrollOffset = scrollbar.value.scrollTop;
  224. const halfViewportHeight = scrollbar.value.clientHeight / 2;
  225. show_menu.value = scrollOffset > halfViewportHeight;
  226. }
  227. };
  228. // 点击按钮,实现滚动到视窗高度距离
  229. const downScroll = () => {
  230. if (scrollbar.value) {
  231. const scrollTop = scrollbar.value.scrollTop;
  232. const viewportHeight = scrollbar.value.clientHeight;
  233. const scrollDistance = viewportHeight - scrollTop - 48;
  234. setTimeout(() => {
  235. scrollbar.value!.scrollBy({ top: scrollDistance, behavior: 'smooth' });
  236. }, 300);
  237. }
  238. };
  239. /**
  240. * 左侧栏
  241. */
  242. // 折叠面板
  243. const activeKey = ref(['']);
  244. const customStyle = 'background: #F5F5F5;border-radius: 4px;margin-bottom: 12px;border: 0;overflow: hidden';
  245. interface panelList {
  246. id: number,
  247. header: string,
  248. text: string
  249. }
  250. const panels = ref<panelList[]>([])
  251. const panelData = async () => {
  252. try {
  253. const response = await get("/classtic/list");
  254. if (response) {
  255. panels.value = response.data.data.map((item: any) => ({
  256. id: item.id,
  257. header: item.header,
  258. text: item.TEXT,
  259. }));
  260. } else {
  261. console.error("Response data structure is not as expected:");
  262. }
  263. } catch (error) {
  264. console.error("Failed to fetch data", error);
  265. }
  266. }
  267. // console.log(panels)
  268. // 社交按钮
  269. const buttons = ref([
  270. { icon: iconComponents.CravatarLined, url: 'https://cravatar.cn/' },
  271. { icon: iconComponents.QQLined, url: '/qqcode' },
  272. { icon: iconComponents.WechatLined, url: '/wechatcode' },
  273. { icon: iconComponents.MusicLined, url: 'https://music.163.com/#/playlist?id=160266689' },
  274. { icon: iconComponents.GitHubLined, url: 'https://gitee.com/c_panda' },
  275. ]);
  276. const handleClick = (url: string) => {
  277. window.open(url)
  278. }
  279. // 链接按钮
  280. interface comlinkList {
  281. id: number,
  282. linktext: string,
  283. linkurl: string
  284. }
  285. const linkbuttons = ref<comlinkList[]>([])
  286. const comLinkData = async () => {
  287. try {
  288. const response = await get("/comlink/list");
  289. if (response) {
  290. linkbuttons.value = response.data.data.map((items: any) => ({
  291. id: items.id,
  292. linktext: items.linktext,
  293. linkurl: items.linkurl
  294. }))
  295. } else {
  296. console.log("response data is not exist")
  297. }
  298. } catch (error) {
  299. console.error("Failed to fetch data", error);
  300. }
  301. }
  302. const comLinkClick = (url: string) => {
  303. if (url.startsWith('http://') || url.startsWith('https://')) {
  304. window.open (url,"_blank"); // 使用 window.location.href 打开外部链接
  305. } else {
  306. router.push(url); // 否则使用 Vue Router 的 push 方法导航
  307. }
  308. }
  309. /**
  310. * 右侧栏
  311. */
  312. // 统计
  313. const statistics = reactive([
  314. {
  315. id: "1",
  316. title: "DAYS",
  317. counts: "111"
  318. },
  319. {
  320. id: "2",
  321. title: "DIARYS",
  322. counts: "111"
  323. },
  324. {
  325. id: "3",
  326. title: "BLOGS",
  327. counts: "111"
  328. },
  329. ])
  330. // 日历热力图
  331. const heat = ref(null);
  332. const newData = <any>[];
  333. // 初始化60天的数据
  334. const data = generateDates(60);
  335. console.log(data)
  336. // 重新排列数据
  337. for (let i = 0; i < 60; i += 15) {
  338. // 取出每个15天的数据,并反转顺序
  339. const chunk = data.slice(i, i + 15).reverse();
  340. newData.push(...chunk);
  341. }
  342. function generateDates(numDays: number) {
  343. const dates = [];
  344. const currentDate = new Date();
  345. for (let i = 0; i < numDays; i++) {
  346. const date = new Date(currentDate);
  347. date.setDate(currentDate.getDate() - i);
  348. dates.push(
  349. { date: date.toISOString().split('T')[0], writCount: 0 }
  350. ); // 包含日期和评论数
  351. }
  352. return dates;
  353. }
  354. const writList = [5, 4, 3, 2, 1, 0]
  355. writList.forEach((item, index) => {
  356. data[index].writCount = item;
  357. })
  358. const formattedData = newData.map((value: any, index: number) => [index % 15, Math.floor(index / 15), value.writCount]);
  359. const heatMapData = {
  360. tooltip: {
  361. position: 'top',
  362. formatter: function (params: any) {
  363. const item = newData[params.dataIndex];
  364. if (item.writCount > 0) {
  365. return `${item.date}<br/>COMMENTS: ${item.writCount}`;
  366. } else {
  367. return `${item.date}`;
  368. }
  369. }
  370. },
  371. grid: {
  372. left: '0', // 左边距
  373. right: '0', // 右边距
  374. top: '0', // 上边距
  375. bottom: '0',
  376. },
  377. xAxis: {
  378. type: 'category',
  379. data: Array.from({ length: 15 }, (_, i) => `Col ${i + 1}`),
  380. splitArea: {
  381. show: true
  382. },
  383. show: false
  384. },
  385. yAxis: {
  386. type: 'category',
  387. data: ['Row 1', 'Row 2', 'Row 3', 'Row 4'],
  388. splitArea: {
  389. show: false
  390. },
  391. show: false
  392. },
  393. visualMap: {
  394. min: 0,
  395. max: 10,
  396. calculable: true,
  397. orient: 'horizontal',
  398. left: 'center',
  399. bottom: '15%',
  400. show: false,
  401. inRange: {
  402. color: ['rgba(182,181,178,0.01)', 'rgb(157,156,153,1)'] // 0评论是白色, 非0评论是黑色
  403. }
  404. },
  405. series: [{
  406. type: 'heatmap',
  407. data: formattedData,
  408. itemStyle: {
  409. borderColor: 'rgb(231,229,225,0.5)', // 设置边框颜色
  410. borderWidth: 0.5, // 设置边框宽度
  411. },
  412. label: {
  413. show: false,
  414. },
  415. emphasis: {
  416. itemStyle: {
  417. shadowBlur: 10,
  418. shadowColor: 'rgba(0, 0, 0, 0.5)',
  419. borderColor: '#fff', // 鼠标悬停时的边框颜色
  420. borderWidth: 2 // 鼠标悬停时的边框宽度
  421. }
  422. }
  423. }]
  424. };
  425. // 音乐组件
  426. const random = ref();
  427. onMounted(() => {
  428. scrollbar.value = document.querySelector('.simplebar-content-wrapper');
  429. panelData()
  430. comLinkData()
  431. nextTick(() => {
  432. const authorElement = document.querySelector('.author');
  433. if (authorElement) {
  434. new Typed('.author', {
  435. strings: ['SunFree.'],
  436. typeSpeed: 200,
  437. backSpeed: 150,
  438. loop: true,
  439. loopCount: Infinity,
  440. showCursor: false
  441. });
  442. };
  443. const cardTextElement = document.querySelector('.cardText');
  444. if (cardTextElement) {
  445. // 定义个人座右铭
  446. new Typed('.cardText', {
  447. strings: ['做三流测试,品瀚霖人生!'],
  448. typeSpeed: 200,
  449. backSpeed: 150,
  450. loop: true, // 开启循环
  451. loopCount: Infinity, // 无限循环
  452. showCursor: false // 取消光标
  453. });
  454. };
  455. const aplayerContainer = document.getElementById('aplayer');
  456. if (aplayerContainer) {
  457. new APlayer({
  458. container: aplayerContainer,
  459. mini: false,
  460. autoplay: false,
  461. theme: '#FADFA3',
  462. loop: 'all',
  463. order: 'random',
  464. preload: 'auto',
  465. volume: 0.7,
  466. mutex: true,
  467. listFolded: true,
  468. listMaxHeight: 90,
  469. lrcType: 3,
  470. audio: [
  471. {
  472. name: 'name1',
  473. artist: 'artist1',
  474. url: 'url1.mp3',
  475. cover: 'cover1.jpg',
  476. lrc: 'lrc1.lrc',
  477. theme: '#ebd0c2'
  478. },
  479. {
  480. name: 'name2',
  481. artist: 'artist2',
  482. url: 'url2.mp3',
  483. cover: 'cover2.jpg',
  484. lrc: 'lrc2.lrc',
  485. theme: '#46718b'
  486. }
  487. ]
  488. });
  489. }
  490. });
  491. createEcharts(heat, heatMapData);
  492. })
  493. watch(
  494. () => route.name,
  495. (newRouteName) => {
  496. updateCarouselVisibility(newRouteName);
  497. if (newRouteName) {
  498. const menuItem = items.value.find(item => item.key === newRouteName);
  499. if (menuItem) {
  500. current.value = [menuItem.key];
  501. } else {
  502. current.value = ['home']; // 如果找不到对应的菜单项,默认选中 'home'
  503. }
  504. } else {
  505. current.value = ['home']; // 如果 route.name 不存在,默认选中 'home'
  506. }
  507. },
  508. { immediate: true }
  509. );
  510. </script>
  511. <style scoped>
  512. .headerMenu {
  513. display: flex;
  514. justify-content: center;
  515. align-items: center;
  516. width: 100%;
  517. height: 48px;
  518. border-bottom: 1px solid rgba(5, 5, 5, 0.06);
  519. position: fixed;
  520. background-color: white;
  521. transform: translateX(-50%);
  522. top: 0;
  523. left: 50%;
  524. z-index: 999;
  525. }
  526. /* 作者名称 */
  527. .author {
  528. position: absolute;
  529. display: inline-block;
  530. font-size: 100px;
  531. font-family: 'Courier New', Courier, monospace;
  532. background: linear-gradient(270deg, #f5f4f2, #d6cdc7, #4b4949);
  533. background-size: 400% 400%;
  534. -webkit-background-clip: text;
  535. color: transparent;
  536. background-clip: text;
  537. animation: salon-light-animation 3s ease infinite;
  538. top: 50%;
  539. left: 50%;
  540. transform: translateX(-50%);
  541. z-index: 999;
  542. }
  543. @keyframes salon-light-animation {
  544. 0% {
  545. background-position: 0% 50%;
  546. }
  547. 50% {
  548. background-position: 100% 50%;
  549. }
  550. 100% {
  551. background-position: 0% 50%;
  552. }
  553. }
  554. .carousel img {
  555. height: 100vh;
  556. width: 100vw;
  557. }
  558. .anchorDown {
  559. position: absolute;
  560. bottom: 100px;
  561. left: 50%;
  562. border-style: hidden;
  563. transform: translateX(-50%);
  564. }
  565. .mainContainer {
  566. display: flex;
  567. justify-content: center;
  568. padding-top: 48px;
  569. background-color: rgba(5, 5, 5, 0.08);
  570. }
  571. .leftBar {
  572. width: 15%;
  573. }
  574. .leftBar img {
  575. /* 图片自适应容器并保持宽高比例 */
  576. aspect-ratio: 1/1;
  577. }
  578. .leftBar>* {
  579. margin-bottom: 24px;
  580. }
  581. .leftBar h1 {
  582. text-align: center;
  583. font-family: Georgia, 'Times New Roman', Times, serif;
  584. }
  585. .leftBar .cardText {
  586. min-height: 60px;
  587. text-align: center;
  588. }
  589. .leftBar>:first-child .button-group {
  590. display: flex;
  591. margin: 0 12px 24px 12px;
  592. justify-content: space-between;
  593. }
  594. .leftBar>:nth-child(2) .button-group {
  595. display: flex;
  596. flex-wrap: wrap;
  597. justify-content: space-between;
  598. }
  599. .leftBar>:nth-child(2) .button-group>* {
  600. width: 40%;
  601. margin: 12px;
  602. }
  603. .rightBar {
  604. width: 15%;
  605. }
  606. .rightBar>* {
  607. margin-bottom: 24px;
  608. }
  609. .rightBar>:nth-child(4) div {
  610. display: flex;
  611. justify-content: center;
  612. position: relative;
  613. margin-bottom: 24px;
  614. }
  615. .rightBar>:nth-child(4) div .article-text {
  616. display: flex;
  617. flex-direction: column;
  618. position: absolute;
  619. bottom: 5%;
  620. left: 10%;
  621. color: rgb(187, 185, 187);
  622. }
  623. #aplayer {
  624. margin: 0;
  625. }
  626. .statistic {
  627. text-align: center;
  628. }
  629. </style>