feat: 添加NProgress加载进度条和优化UI样式

添加NProgress依赖并集成到路由中,实现页面切换时的加载进度条
优化友链页面、博客分类和文章列表的UI样式
调整登录按钮和图片懒加载的显示效果
新增标签随机颜色功能,提升视觉体验
This commit is contained in:
2026-01-15 17:17:40 +08:00
parent d00c18d38a
commit ac4f8dac82
17 changed files with 204 additions and 73 deletions

1
components.d.ts vendored
View File

@ -33,6 +33,7 @@ declare module 'vue' {
NLayoutFooter: typeof import('naive-ui')['NLayoutFooter'] NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader'] NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMenu: typeof import('naive-ui')['NMenu'] NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider'] NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal'] NModal: typeof import('naive-ui')['NModal']

1
extra.d.ts vendored
View File

@ -4,3 +4,4 @@ declare module "vue3-masonry-plus";
declare module "vite"; declare module "vite";
declare module "vue-devui/tag"; declare module "vue-devui/tag";
declare module "es-toolkit"; declare module "es-toolkit";
declare module "nprogress";

View File

@ -40,6 +40,7 @@
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"naive-ui": "^2.43.2", "naive-ui": "^2.43.2",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"nprogress": "^0.2.0",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"vfonts": "^0.0.3", "vfonts": "^0.0.3",
"vite": "^7.0.6", "vite": "^7.0.6",

View File

@ -1,5 +1,6 @@
<template> <template>
<n-config-provider :theme-overrides="themeOverrides"> <n-config-provider :theme-overrides="themeOverrides">
<n-loading-bar-provider>
<n-message-provider> <n-message-provider>
<n-dialog-provider> <n-dialog-provider>
<n-modal-provider> <n-modal-provider>
@ -9,19 +10,23 @@
</n-modal-provider> </n-modal-provider>
</n-dialog-provider> </n-dialog-provider>
</n-message-provider> </n-message-provider>
</n-config-provider> </n-loading-bar-provider>
</n-config-provider>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import themeOverrides from '@/util/theme.ts'; import themeOverrides from '@/util/theme.ts';
import layIndex from './Index.vue'; import layIndex from './Index.vue';
// 空白项目入口 // 空白项目入口
const logStatus = $store.log.useLogStore() const logStatus = $store.log.useLogStore()
// 全局禁用右键菜单 // 全局禁用右键菜单
document.oncontextmenu = function () { document.oncontextmenu = function () {
return false; return false;
}; };
onMounted(() => { onMounted(() => {
if ($cookies.get('userinfo')) logStatus.setIsLogin(true) if ($cookies.get('userinfo')) logStatus.setIsLogin(true)
}) })
</script> </script>

View File

@ -10,3 +10,13 @@
.n-scrollbar-rail__scrollbar { .n-scrollbar-rail__scrollbar {
background-color: #f6cbe770 !important; background-color: #f6cbe770 !important;
} }
/* 添加渐变背景色 */
#nprogress .bar {
background: linear-gradient(to right, #ec66ab, #f78c6c, #7ed6df);
}
/* 添加平滑过渡效果 */
#nprogress .bar {
transition: width 0.2s ease-in-out;
}

View File

@ -1,3 +1,5 @@
@import "./base.less";
@import "qweather-icons/font/qweather-icons.css"; @import "qweather-icons/font/qweather-icons.css";
@import 'md-editor-v3/lib/preview.css'; @import 'md-editor-v3/lib/preview.css';
@import "nprogress/nprogress.css";
@import "./base.less";

View File

@ -31,8 +31,7 @@
</div> </div>
</n-dropdown> </n-dropdown>
<div v-else class="flex items-center" @click="toLogin"> <div v-else class="flex items-center" @click="toLogin">
<n-avatar round class="cursor-pointer"></n-avatar> <n-button size="small" type="primary" @click="toLogin">登录</n-button>
<div class="cursor-pointer ml-2 text-gray text-sm">登录</div>
</div> </div>
</div> </div>
<!-- 登录弹窗 --> <!-- 登录弹窗 -->
@ -51,8 +50,10 @@
<div class="cursor-pointer ml-2 text-gray text-sm">{{ userinfo.nickname }}</div> <div class="cursor-pointer ml-2 text-gray text-sm">{{ userinfo.nickname }}</div>
</div> </div>
<div v-else class="flex items-center"> <div v-else class="flex items-center">
<n-avatar round class="cursor-pointer" @click="toLogin"></n-avatar> <!-- <n-avatar round class="cursor-pointer" @click="toLogin"></n-avatar>
<div class="cursor-pointer ml-2 text-gray text-sm" @click="toLogin">登录</div> <div class="cursor-pointer ml-2 text-gray text-sm" @click="toLogin">登录</div> -->
<n-button type="primary" size="small" @click="toLogin">登录</n-button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1 +1,4 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767254162122" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5014" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M661.648 118H249.104c-39.088 0-70.304 31.752-70.304 70.304v647.392c0 39.096 31.752 70.304 70.304 70.304h525.8c38.552 0 70.296-31.752 70.296-70.304V301.56L661.648 118z m12.664 74.704l96.176 96.184H684.584c-5.408 0-10.272-4.864-10.272-10.272V192.704z m126.928 642.992a26.24 26.24 0 0 1-26.344 26.352H249.104a26.24 26.24 0 0 1-26.344-26.352V188.304a26.24 26.24 0 0 1 26.344-26.344v-0.464h381.328v117.192c0 29.824 24.416 54.16 54.16 54.16h116.656v502.848z" p-id="5015"></path><path d="M348.68 386.072h188.968a21.84 21.84 0 0 0 21.944-21.936 21.84 21.84 0 0 0-21.944-21.944H348.68a21.832 21.832 0 0 0-21.944 21.944 21.84 21.84 0 0 0 21.944 21.936zM674.848 644.8H348.68c-12.208 0-21.944 9.744-21.944 21.944s9.736 21.944 21.944 21.944h326.168c12.208 0 21.944-9.744 21.944-21.944S687.064 644.8 674.848 644.8zM348.68 495.856a21.824 21.824 0 0 0-21.944 21.944 21.832 21.832 0 0 0 21.944 21.936h326.168a21.832 21.832 0 0 0 21.944-21.936 21.832 21.832 0 0 0-21.944-21.944H348.68z" p-id="5016"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z" clip-rule="evenodd" />
<path d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 652 B

View File

@ -2,3 +2,4 @@
<path fill-rule="evenodd" d="M9.75 6.75h-3a3 3 0 0 0-3 3v7.5a3 3 0 0 0 3 3h7.5a3 3 0 0 0 3-3v-7.5a3 3 0 0 0-3-3h-3V1.5a.75.75 0 0 0-1.5 0v5.25Zm0 0h1.5v5.69l1.72-1.72a.75.75 0 1 1 1.06 1.06l-3 3a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 1 1 1.06-1.06l1.72 1.72V6.75Z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M9.75 6.75h-3a3 3 0 0 0-3 3v7.5a3 3 0 0 0 3 3h7.5a3 3 0 0 0 3-3v-7.5a3 3 0 0 0-3-3h-3V1.5a.75.75 0 0 0-1.5 0v5.25Zm0 0h1.5v5.69l1.72-1.72a.75.75 0 1 1 1.06 1.06l-3 3a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 1 1 1.06-1.06l1.72 1.72V6.75Z" clip-rule="evenodd" />
<path d="M7.151 21.75a2.999 2.999 0 0 0 2.599 1.5h7.5a3 3 0 0 0 3-3v-7.5c0-1.11-.603-2.08-1.5-2.599v7.099a4.5 4.5 0 0 1-4.5 4.5H7.151Z" /> <path d="M7.151 21.75a2.999 2.999 0 0 0 2.599 1.5h7.5a3 3 0 0 0 3-3v-7.5c0-1.11-.603-2.08-1.5-2.599v7.099a4.5 4.5 0 0 1-4.5 4.5H7.151Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 530 B

View File

@ -1 +1,3 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1767231816303" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12438" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M788.48 629.418667c0-17.066667-5.802667-31.744-17.749333-44.714667L421.205333 235.861333c-12.288-12.288-29.354667-22.869333-49.834667-31.744-21.162667-8.874667-39.936-12.970667-57.002667-12.970667L110.933333 191.146667c-17.066667 0-31.744 6.485333-44.032 18.773333-12.288 12.288-18.773333 26.965333-18.773333 44.032l0 202.752c0 17.066667 4.096 36.522667 12.970667 57.002667 8.874667 21.162667 18.773333 37.546667 31.744 49.834667l349.525333 349.525333c12.288 12.288 26.965333 17.749333 44.032 17.749333 17.066667 0 31.744-5.802667 44.714667-17.749333l239.616-239.616C781.994667 661.162667 788.48 646.485333 788.48 629.418667L788.48 629.418667 788.48 629.418667zM248.490667 392.192c-12.288 12.288-26.965333 18.090667-44.032 18.090667-17.066667 0-31.744-5.802667-44.032-18.090667-12.288-12.288-18.090667-26.965333-18.090667-44.032 0-17.066667 5.802667-31.744 18.090667-44.032 12.288-12.288 26.965333-18.090667 44.032-18.090667 17.066667 0 31.744 5.802667 44.032 18.090667C260.778667 316.416 266.24 331.093333 266.24 348.16 267.264 365.568 260.778667 379.904 248.490667 392.192L248.490667 392.192 248.490667 392.192zM958.122667 584.362667 608.597333 235.861333c-12.288-12.288-29.354667-22.869333-49.834667-31.744C537.6 195.242667 518.826667 191.146667 501.76 191.146667l-109.909333 0c17.066667 0 36.522667 4.096 57.002667 12.970667 21.162667 8.874667 37.546667 18.773333 49.834667 31.744l349.525333 348.501333c12.288 12.970667 18.090667 27.648 18.090667 44.714667s-5.802667 31.744-18.090667 44.032l-229.034667 229.717333c9.898667 9.898667 18.773333 17.066667 25.941333 21.845333 7.168 4.778667 17.066667 6.485333 28.672 6.485333 17.066667 0 31.744-5.802667 44.714667-18.090667l239.616-240.298667c12.288-12.288 17.749333-26.965333 17.749333-44.032S970.069333 597.674667 958.122667 584.362667L958.122667 584.362667 958.122667 584.362667zM958.122667 584.362667" fill="#a7415d" p-id="12439"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M5.25 2.25a3 3 0 0 0-3 3v4.318a3 3 0 0 0 .879 2.121l9.58 9.581c.92.92 2.39 1.186 3.548.428a18.849 18.849 0 0 0 5.441-5.44c.758-1.16.492-2.629-.428-3.548l-9.58-9.581a3 3 0 0 0-2.122-.879H5.25ZM6.375 7.5a1.125 1.125 0 1 0 0-2.25 1.125 1.125 0 0 0 0 2.25Z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 411 B

View File

@ -2,7 +2,7 @@
<div class="image-container flex w-full"> <div class="image-container flex w-full">
<n-image ref="lazyRef" class="lazy__img" :src="url" @load="handleLoad" @error="handleError"> <n-image ref="lazyRef" class="lazy__img" :src="url" @load="handleLoad" @error="handleError">
<template #placeholder> <template #placeholder>
<img :src="loading" alt="loading" /> <icon-loading class="zhuan text-primary w-12 h-12" />
</template> </template>
<template #error> <template #error>
<img :src="errorImg" alt="error" /> <img :src="errorImg" alt="error" />
@ -74,4 +74,21 @@ onMounted(() => {
padding: 1em; padding: 1em;
margin: 0 auto; margin: 0 auto;
} }
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.zhuan {
animation: spin 1.5s linear infinite;
}
</style> </style>

View File

@ -3,20 +3,19 @@ import { useConfig } from "@/config";
import icon from "@/icon/index.ts"; import icon from "@/icon/index.ts";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import "virtual:uno.css"; import "virtual:uno.css";
import "vue-devui/tag/style.css"; import 'vue-devui/tag/style.css';
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
// 自定义主题配置 - 设置主色和二级色 // 自定义主题配置 - 设置主色和二级色\
import backTop from "@/components/backTop.vue"; import backTop from '@/components/backTop.vue';
import { formatTime } from "@/util"; import 'md-editor-v3/lib/style.css';
import "md-editor-v3/lib/style.css";
import "vfonts/FiraCode.css"; import "vfonts/FiraCode.css";
import Tag from "vue-devui/tag"; import Tag from 'vue-devui/tag';
const app = createApp(App); const app = createApp(App);
app.use(Tag); app.use(Tag)
app.use(createPinia()); app.use(createPinia());
app.use(router); app.use(router);
// app.use(VMdPreview) // app.use(VMdPreview)

View File

@ -1,6 +1,6 @@
import NProgress from 'nprogress';
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import { routes } from 'vue-router/auto-routes'; import { routes } from "vue-router/auto-routes";
routes.unshift({ routes.unshift({
path: "/", path: "/",
redirect: "/home", redirect: "/home",
@ -8,7 +8,17 @@ routes.unshift({
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes:routes as any routes: routes as any,
});
// 前置守卫
router.beforeEach((to, from, next) => {
NProgress.start()
next();
});
// 后置守卫
router.afterEach(() => {
NProgress.done()
}); });
export default router; export default router;

View File

@ -25,8 +25,8 @@
<div <div
class="absolute rounded-md flex px-2 z-10 truncate bottom-0 w-full bg-[#00000070] text-white justify-between items-center"> class="absolute rounded-md flex px-2 z-10 truncate bottom-0 w-full bg-[#00000070] text-white justify-between items-center">
<div> <span class="text-[#f1d9db] font-600">{{ item.nickname }}</span> 分享</div> <div> <span class="text-[#f1d9db] font-600">{{ item.nickname }}</span> 分享</div>
<div class="text-sm cursor-pointer text-primary" @click="downloadFile(item.filepath)"> <div class="text-sm cursor-pointer text-[#f6cbe7] flex items-center" @click="downloadFile(item.filepath)">
<icon-download class="w-4 h-4" /> <icon-download class="w-5 h-5" />
下载 下载
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
<n-card class="card w-100 shadow"> <n-card class="card w-100 shadow">
<template #header> <template #header>
<div class="flex items-center gap-4 text-primary text-xl font-bold"> <div class="flex items-center gap-4 text-primary text-xl font-bold">
<icon-loading class="zhuan text-primary w-6 h-6" /> <icon-loading class="zhuan text-primary w-6 h-6" />
申请友链 申请友链
</div> </div>
</template> </template>
@ -54,8 +54,8 @@
站点介绍 站点介绍
</div> </div>
</template> </template>
<n-input type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" maxlength="50" v-model:value="plinkData.desc" <n-input type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" maxlength="50"
placeholder="请用一句话简短的描述你的站点" /> v-model:value="plinkData.desc" placeholder="请用一句话简短的描述你的站点" />
</n-form-item> </n-form-item>
<n-form-item path="tagname"> <n-form-item path="tagname">
<template #label> <template #label>
@ -64,8 +64,8 @@
标签 标签
</div> </div>
</template> </template>
<n-input type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" maxlength="50" v-model:value="plinkData.tagname" <n-input type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" maxlength="50"
placeholder="输入你的标签用逗号隔开注意每个标签最多3个字符且只显示前3个标签哦~" /> v-model:value="plinkData.tagname" placeholder="输入你的标签用逗号隔开注意每个标签最多3个字符且只显示前3个标签哦~" />
</n-form-item> </n-form-item>
<n-form-item> <n-form-item>
<n-button class="w-full" type="primary" @click="submitPut">申请友链</n-button> <n-button class="w-full" type="primary" @click="submitPut">申请友链</n-button>
@ -83,13 +83,16 @@
<div class="ava"> <div class="ava">
<n-avatar round size="large" :src="p.avater"></n-avatar> <n-avatar round size="large" :src="p.avater"></n-avatar>
</div> </div>
<div class="text-primary"> <em class="bg-[#fbf2e3] px-4 rounded-full text-sm text-primary mb-4">{{ p.name }}</em>
<div class="font-bold text-xl text-gray-700 mb-1">
{{ p.title }} {{ p.title }}
</div> </div>
<em class="text-gray-400 text-sm">{{ p.name }}</em>
<div class="flex gap-1 truncate"> <div class="flex gap-1 truncate mb-1 text-sm">
<div v-for="i in getTags(p.tagname)" :key="i.tid" class="flex items-center gap-1 text-deepp text-sm"> <div v-for="i in getTags(p.tagname)" :key="i.tid"
<icon-tag2 class="w-3 h-3 !text-deepp" />{{ i }} :style="{ backgroundColor: getDictValue(tagColorList, 'tag', i, 'color') }"
class="flex items-center gap-1 cursor-pointer text-sm text-white rounded-full px-3">
<icon-tag2 class="w-3 h-3 " />{{ i }}
</div> </div>
</div> </div>
<div class="truncate w-full text-gray-400 text-sm text-center"> <div class="truncate w-full text-gray-400 text-sm text-center">
@ -108,7 +111,7 @@ import siteIcon from '@/icon/plink/site.svg';
import tagIcon from '@/icon/plink/tag.svg'; import tagIcon from '@/icon/plink/tag.svg';
import urlIcon from '@/icon/plink/uri.svg'; import urlIcon from '@/icon/plink/uri.svg';
import userIcon from '@/icon/plink/user.svg'; import userIcon from '@/icon/plink/user.svg';
import { getDictValue } from '@/util';
definePage({ definePage({
name: 'plink', name: 'plink',
@ -137,18 +140,32 @@ const rules: any = {
} }
const pList = ref<any>([]) const pList = ref<any>([])
const formRef = ref<any>(null) const formRef = ref<any>(null)
const tagColorList: any = ref([])
async function getPlinkList() { async function getPlinkList() {
const res = await $http.plink.getPlinkList() const res = await $http.plink.getPlinkList()
const list: Array<string> = []
// 对res.data.tags进行去重
res.data.forEach((i: any) => {
const t = getTags(i.tagname)
t.forEach((ii: string) => {
if (!list.includes(ii)) list.push(ii)
})
});
tagColorList.value = list.map((i: string) => {
return {
tag: i,
color: getRandomFromPalette()
}
})
pList.value = res.data pList.value = res.data
// for (let i = 0; i < 20; i++) {
// pList.value.push(res.data[0])
// }
} }
function getTags(str: any) { function getTags(str: any) {
str = str.replaceAll('', ',') str = str.replaceAll('', ',')
let tags = str.split(',').slice(0, 3) let tags = str.split(',').slice(0, 3)
tags.forEach((tag: any, idx: number) => { tags.forEach((tag: any, idx: number) => {
tags[idx] = tag.trim().slice(0, 3) tags[idx] = tag.trim().slice(0, 4)
}); });
return tags return tags
} }
@ -178,7 +195,23 @@ function submitPut() {
}) })
} }
// 生成一个随机的鲜艳颜色,和白色能对比
function getRandomFromPalette(): string {
const hue = Math.floor(Math.random() * 360);
return hslToHex(hue, 80, 50);
}
function hslToHex(h: number, s: number, l: number): any {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
}
return `#${f(0)}${f(8)}${f(4)}`;
}
onMounted(() => { onMounted(() => {
boxStyle.value = { boxStyle.value = {
height: window.innerHeight - nav.navH - 1 + 'px', height: window.innerHeight - nav.navH - 1 + 'px',
@ -197,11 +230,8 @@ onMounted(() => {
100% { 100% {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.zhuan { .zhuan {
animation: spin 1.5s linear infinite; animation: spin 1.5s linear infinite;
} }

View File

@ -4,11 +4,13 @@
<div class="card w-2/3 absolute top-16 left-1/6"> <div class="card w-2/3 absolute top-16 left-1/6">
<div @click="clickCate(i, idx)" v-for="(i, idx) in cateList" :key="i.cid" <div @click="clickCate(i, idx)" v-for="(i, idx) in cateList" :key="i.cid"
:class="{ '!bg-primary text-white': idx == currentCateIdx }" :class="{ '!bg-primary text-white': idx == currentCateIdx }"
class="relative cate my-4 py-8 px-16 text-center text-xl font-bold rounded text-gray-600 bg-gray-200 cursor-pointer"> class="relative cate flex my-6 py-6 px-8 justify-between text-xl font-bold bg-white shadow rounded cursor-pointer">
{{ i.name }} <div class="flex items-center">
<div class="absolute bottom-1 right-4 ext-sm text-deepp flex items-center gap-2"> <icon-arti class="w-5 h-5 text-primary mr-3" :class="{ 'text-white': idx == currentCateIdx }"></icon-arti>
<icon-arti class="w-4 h-4 !fill-deepp"></icon-arti> <span>{{ i.name }}</span>
{{ i.total || 0 }}篇文章 </div>
<div class="bg-primary px-4 rounded-full text-white ext-sm flex items-center gap-2">
{{ i.total || 0 }}
</div> </div>
</div> </div>
</div> </div>
@ -36,12 +38,14 @@
<div class="text-xl font-bold text-primary mb-2">{{ item.title }}</div> <div class="text-xl font-bold text-primary mb-2">{{ item.title }}</div>
<div class="text-sm text-gray-500 mb-4 line-clamp-3">{{ item.pro }}</div> <div class="text-sm text-gray-500 mb-4 line-clamp-3">{{ item.pro }}</div>
<div class="flex gap-4"> <div class="flex gap-4">
<div v-for="i in getTags(item.tags)" :key="i.tid" class="flex items-center gap-1 text-deepp cursor-pointer"> <div v-for="i in getTags(item.tags)" :key="i.tid"
<icon-tag2 class="w-4 h-4 !text-deepp" />{{ i }} :style="{ backgroundColor: getDictValue(tagColorList, 'tag', i, 'color') }"
class="flex items-center gap-1 cursor-pointer text-white rounded-full px-3">
<icon-tag2 class="w-4 h-4" />{{ i }}
</div> </div>
</div> </div>
</div> </div>
<div v-show="blogList.length != 0" class="mt-20 w-[80%] ml-1/10 cursor-pointer flex justify-center"> <div v-show="blogList.length > page_size" class="mt-20 w-[80%] ml-1/10 cursor-pointer flex justify-center">
<n-pagination v-model:page="page_num" :page-count="total" :page-slot="5" @update:page="getBlogList" /> <n-pagination v-model:page="page_num" :page-count="total" :page-slot="5" @update:page="getBlogList" />
</div> </div>
</div> </div>
@ -49,13 +53,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { formatTime } from '@/util' import { formatTime, getDictValue } from '@/util'
definePage({ definePage({
name: 'blog', name: 'blog',
meta: { meta: {
title: '文章', title: '文章',
} }
}) })
const blogList = ref<any[]>([]) const blogList = ref<any[]>([])
const boxStyle = ref({}) const boxStyle = ref({})
const nav: any = $store.nav.useNavStore() const nav: any = $store.nav.useNavStore()
@ -65,6 +70,9 @@ const page_size = ref(5)
const page_num = ref(1) const page_num = ref(1)
const total = ref(0) const total = ref(0)
const category = ref('') const category = ref('')
const tagColorList: any = ref([])
function clickCate(item: any, idx: number) { function clickCate(item: any, idx: number) {
if (currentCateIdx.value == idx) { if (currentCateIdx.value == idx) {
currentCateIdx.value = -1 currentCateIdx.value = -1
@ -82,8 +90,25 @@ function clickCate(item: any, idx: number) {
async function getBlogList() { async function getBlogList() {
const res = await $http.blog.getBlogList({ const res = await $http.blog.getBlogList({
category:category.value,page_size:page_size.value,page_num:page_num.value category: category.value, page_size: page_size.value, page_num: page_num.value
}) })
const list: Array<string> = []
// 对res.data.tags进行去重
res.data.forEach((i: any) => {
const t = getTags(i.tags)
t.forEach((ii: string) => {
if (!list.includes(ii)) list.push(ii)
})
});
tagColorList.value = list.map((i: string) => {
return {
tag: i,
color: getRandomFromPalette()
}
})
blogList.value = res.data blogList.value = res.data
} }
// 计算markdown的文字数量 // 计算markdown的文字数量
@ -92,28 +117,46 @@ function getSize(str: string) {
return str.replace(reg, '').length; return str.replace(reg, '').length;
} }
function getTags(str: any) { // 生成一个随机的鲜艳颜色,和白色能对比
str = str.replaceAll('', ',')
let tags = str.split(',').slice(0, 5) function getRandomFromPalette(): string {
tags.forEach((tag: any, idx: number) => { const hue = Math.floor(Math.random() * 360);
tags[idx] = tag.trim().slice(0, 3) return hslToHex(hue, 80, 50);
});
return tags
}
async function getCateList() {
const res = await $http.blog.getCateList()
console.log('>>> --> getCateList --> res:', res.data)
cateList.value = res.data
total.value = cateList.value.reduce((acc: any, cur: any) => acc + cur.total, 0)
} }
onMounted(() => { function hslToHex(h: number, s: number, l: number): any {
boxStyle.value = { l /= 100;
height: `calc(100vh - ${nav.navH + 1}px)`, const a = s * Math.min(l, 1 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
} }
getBlogList() return `#${f(0)}${f(8)}${f(4)}`;
getCateList() }
})
function getTags(str: any) {
str = str.replaceAll('', ',')
let tags = str.split(',').slice(0, 5)
tags.forEach((tag: any, idx: number) => {
tags[idx] = tag.trim().slice(0, 4)
});
return tags
}
async function getCateList() {
const res = await $http.blog.getCateList()
console.log('>>> --> getCateList --> res:', res.data)
cateList.value = res.data
total.value = cateList.value.reduce((acc: any, cur: any) => acc + cur.total, 0)
}
onMounted(() => {
boxStyle.value = {
height: `calc(100vh - ${nav.navH + 1}px)`,
}
getBlogList()
getCateList()
})
</script> </script>

View File

@ -3609,6 +3609,11 @@ npm-run-all2@^8.0.4:
shell-quote "^1.7.3" shell-quote "^1.7.3"
which "^5.0.0" which "^5.0.0"
nprogress@^0.2.0:
version "0.2.0"
resolved "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==
nth-check@^2.0.1: nth-check@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" resolved "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"