✨ 升级到Nuxt4
✨ 完成分类 标签的HTML搭建 🐛 修复ssg相关配置 之前错误的使用了SPA 🐛 修复部分ssg兼容问题
This commit is contained in:
parent
75ba2bf5c8
commit
51a39d498b
|
|
@ -0,0 +1,142 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PostMetaData } from '~/types/PostMetaData';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
postsMetaData?: PostMetaData[];
|
||||||
|
}>(), {
|
||||||
|
postsMetaData: () => [],
|
||||||
|
});
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(event: 'filterRuleChange', rule: (data: PostMetaData) => boolean): void;
|
||||||
|
}>();
|
||||||
|
const articleCount = computed(() => props.postsMetaData?.filter((post) => !post.draft && post.type === 'article').length || 0);
|
||||||
|
const announcementCount = computed(() => props.postsMetaData?.filter((post) => !post.draft && post.type === 'announcement').length || 0);
|
||||||
|
const ramblingCount = computed(() => props.postsMetaData?.filter((post) => !post.draft && post.type === 'rambling').length || 0);
|
||||||
|
const countGroup = [
|
||||||
|
{ name: '文章', count: articleCount, type: 'article' },
|
||||||
|
{ name: '絮语', count: ramblingCount, type: 'rambling' },
|
||||||
|
{ name: '公告', count: announcementCount, type: 'announcement' },
|
||||||
|
];
|
||||||
|
const categoryEnableStatus: Ref<boolean[]> = ref(Array(countGroup.length).fill(true));
|
||||||
|
|
||||||
|
const categories = computed(() => {
|
||||||
|
const categoryMap = new Map<string, number>();
|
||||||
|
props.postsMetaData?.forEach((post) => {
|
||||||
|
if (post.category) {
|
||||||
|
categoryMap.set(post.category, (categoryMap.get(post.category) || 0) + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let categoryArray = Array.from(categoryMap.entries());
|
||||||
|
categoryArray = categoryArray.sort((a, b) => b[1] - a[1]);
|
||||||
|
return categoryArray;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tags = computed(() => {
|
||||||
|
const tagMap = new Map<string, number>();
|
||||||
|
props.postsMetaData?.forEach((post) => {
|
||||||
|
post.tags?.forEach((tag) => {
|
||||||
|
tagMap.set(tag, (tagMap.get(tag) || 0) + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let tagArray = Array.from(tagMap.entries());
|
||||||
|
tagArray = tagArray.sort((a, b) => b[1] - a[1]);
|
||||||
|
return tagArray;
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateCategoryEnableStatus(index: number) {
|
||||||
|
if (categoryEnableStatus.value.reduce((last, cur) => last && cur, true)) {
|
||||||
|
for (let i = 0; i < categoryEnableStatus.value.length; i++) {
|
||||||
|
if (i !== index) {
|
||||||
|
categoryEnableStatus.value[i] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!categoryEnableStatus.value.reduce((last, cur, localIndex) => last || (localIndex === index ? false : cur), false)) {
|
||||||
|
for (let i = 0; i < categoryEnableStatus.value.length; i++) {
|
||||||
|
categoryEnableStatus.value[i] = true;
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
categoryEnableStatus.value[index] = !categoryEnableStatus.value[index];
|
||||||
|
updateRule();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRule() {
|
||||||
|
emits('filterRuleChange', (post) => {
|
||||||
|
for (let i = 0; i < categoryEnableStatus.value.length; i++) {
|
||||||
|
if (categoryEnableStatus.value[i] && post.type === countGroup[i]!.type) {
|
||||||
|
console.log('filter', post.title, 'true');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('filter', post.title, 'false');
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="bg-old-neutral-200 dark:bg-old-neutral-800 transition-colors duration-500 p-5">
|
||||||
|
<div class="text-2xl ml-1 flex items-center">
|
||||||
|
<Icon class="mr-2" name="material-symbols:category"/>
|
||||||
|
类型
|
||||||
|
</div>
|
||||||
|
<hr class="border-0 h-[1px] bg-old-neutral-600 mt-3 mb-1"/>
|
||||||
|
<div class="flex mt-4">
|
||||||
|
<div
|
||||||
|
v-for="(data, index) of countGroup"
|
||||||
|
:key="data.name"
|
||||||
|
class="flex items-center flex-col flex-1 text-xl cursor-pointer hover:text-sky-400 dark:hover:text-[#cccaff] transition-colors duration-300"
|
||||||
|
:class="{'text-old-neutral-400': !categoryEnableStatus[index]}"
|
||||||
|
@click="updateCategoryEnableStatus(index)"
|
||||||
|
>
|
||||||
|
<div>{{ data.name }}</div>
|
||||||
|
<div>{{ data.count }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-old-neutral-200 dark:bg-old-neutral-800 transition-colors duration-500 p-5 mt-4">
|
||||||
|
<div class="text-2xl ml-1 flex items-center">
|
||||||
|
<Icon class="mr-2" name="material-symbols:book"/>
|
||||||
|
分类
|
||||||
|
</div>
|
||||||
|
<hr class="border-0 h-[1px] bg-old-neutral-600 mt-3 mb-1"/>
|
||||||
|
<div
|
||||||
|
v-for="([name,count],index) of categories" :key="index"
|
||||||
|
class="flex justify-between pl-4 pr-4 hover:text-sky-400 dark:hover:text-[#cccaff] transition-colors duration-300">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon
|
||||||
|
name="material-symbols:book-outline"
|
||||||
|
size="17"
|
||||||
|
class="mt-0.5 mr-1"
|
||||||
|
/>
|
||||||
|
<div>{{ name }}</div>
|
||||||
|
</div>
|
||||||
|
<div>{{ count }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-old-neutral-200 dark:bg-old-neutral-800 transition-colors duration-500 p-5 mt-4">
|
||||||
|
<div class="text-2xl ml-1 flex items-center">
|
||||||
|
<Icon class="mr-2" name="material-symbols:bookmarks"/>
|
||||||
|
标签
|
||||||
|
</div>
|
||||||
|
<hr class="border-0 h-[1px] bg-old-neutral-600 mt-3 mb-1"/>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div
|
||||||
|
v-for="([name,count],index) of tags" :key="index"
|
||||||
|
class="flex items-center justify-between text-[15px] pl-2 pr-2 m-1 rounded-2xl shadow-[0_0_0_1px_#888] hover:text-sky-400 dark:hover:text-[#cccaff] hover:shadow-[0_0_0_1px_#00bcff] dark:hover:shadow-[0_0_0_1px_#cccaff] transition-colors transition-shadow duration-300">
|
||||||
|
<Icon
|
||||||
|
name="clarity:hashtag-solid"
|
||||||
|
size="17"
|
||||||
|
class="mr-1 "
|
||||||
|
/>
|
||||||
|
<div class="mr-1">{{ name }}</div>
|
||||||
|
<div class="">{{ count }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -1,72 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PostMetaData } from '~/types/PostMetaData';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
postsMetaData?: PostMetaData[];
|
|
||||||
}>(), {
|
|
||||||
postsMetaData: () => [],
|
|
||||||
});
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(event: 'filterRuleChange', rule: string): void;
|
|
||||||
}>();
|
|
||||||
const articleCount = computed(() => props.postsMetaData?.filter((post) => !post.draft && post.type === 'article').length || 0);
|
|
||||||
const announcementCount = computed(() => props.postsMetaData?.filter((post) => !post.draft && post.type === 'announcement').length || 0);
|
|
||||||
const ramblingCount = computed(() => props.postsMetaData?.filter((post) => !post.draft && post.type === 'rambling').length || 0);
|
|
||||||
const countGroup = [
|
|
||||||
{ name: '文章', count: articleCount, type: 'article' },
|
|
||||||
{ name: '絮语', count: ramblingCount, type: 'rambling' },
|
|
||||||
{ name: '公告', count: announcementCount, type: 'announcement' },
|
|
||||||
];
|
|
||||||
const categories = computed(() => {
|
|
||||||
const categoryMap = new Map<string, number>();
|
|
||||||
props.postsMetaData?.forEach((post) => {
|
|
||||||
if (post.category) {
|
|
||||||
categoryMap.set(post.category, (categoryMap.get(post.category) || 0) + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return categoryMap;
|
|
||||||
});
|
|
||||||
let showType = '';
|
|
||||||
|
|
||||||
function ruleChange(name: string) {
|
|
||||||
if (showType === name || name === '') {
|
|
||||||
showType = '';
|
|
||||||
} else {
|
|
||||||
showType = name;
|
|
||||||
}
|
|
||||||
emits('filterRuleChange', showType);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="transition-colors duration-500">
|
<div>
|
||||||
|
<div class="bg-old-neutral-200 dark:bg-old-neutral-800 transition-colors duration-500 p-5">
|
||||||
|
Author: Lichx
|
||||||
<div>
|
<div>
|
||||||
<div v-if="showType === ''" class="flex">
|
Contact me:
|
||||||
<div
|
<a href="mailto:li_chx@qq.com" />
|
||||||
v-for="data of countGroup"
|
|
||||||
:key="data.name"
|
|
||||||
class="flex items-center flex-col flex-1 text-xl cursor-pointer hover:text-sky-300 dark:hover:text-[#cccaff] transition-colors duration-300"
|
|
||||||
@click="ruleChange(data.type)"
|
|
||||||
>
|
|
||||||
<div>{{ data.name }}</div>
|
|
||||||
<div>{{ data.count }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex items-center hover:text-sky-300 dark:hover:text-[#cccaff] transition-colors duration-300" @click="ruleChange('')">
|
|
||||||
<div class="flex-1 text-2xl flex items-center justify-center">
|
|
||||||
<div>{{ countGroup.filter((x) => x.type === showType)[0].name }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 text-2xl flex items-center justify-center">
|
|
||||||
<div class="pr-8">{{ countGroup.filter((x) => x.type === showType)[0].count }}</div>
|
|
||||||
</div>
|
|
||||||
<!-- <Icon-->
|
|
||||||
<!-- name="mingcute:back-line" class="flex-1 text-5xl cursor-pointer dark:hover:text-[#cccaff] hover:text-sky-300 transition-colors duration-300"-->
|
|
||||||
<!-- @click="ruleChange('')"/>-->
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ const props = withDefaults(defineProps<{
|
||||||
}>(), {
|
}>(), {
|
||||||
markdown: () => '## Hello World!',
|
markdown: () => '## Hello World!',
|
||||||
});
|
});
|
||||||
console.log(props.markdown);
|
|
||||||
const eraseHeaderMarkdown = computed(() => props.markdown.replace(/^---[\s\S]*?---\n?/, ''));
|
const eraseHeaderMarkdown = computed(() => props.markdown.replace(/^---[\s\S]*?---\n?/, ''));
|
||||||
|
|
||||||
const { colorMode } = storeToRefs(useColorModeStore());
|
const { colorMode } = storeToRefs(useColorModeStore());
|
||||||
|
|
@ -18,7 +17,9 @@ const { colorMode } = storeToRefs(useColorModeStore());
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="pt-0 bg-old-neutral-200 dark:bg-old-neutral-800 transition-colors duration-500">
|
<div class="pt-0 bg-old-neutral-200 dark:bg-old-neutral-800 transition-colors duration-500">
|
||||||
<MdPreview :editor-id="editorId" :theme="colorMode" :model-value="eraseHeaderMarkdown" class="transition-all duration-500 max-w-full"/>
|
<client-only>
|
||||||
|
<MdPreview :editor-id="editorId" :theme="colorMode" :model-value="eraseHeaderMarkdown" class="transition-all duration-500 max-w-full"/>
|
||||||
|
</client-only>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const renderChart = () => {
|
||||||
const techStackPercent = props.techStackPercent as number[];
|
const techStackPercent = props.techStackPercent as number[];
|
||||||
if (!chartRef.value) return;
|
if (!chartRef.value) return;
|
||||||
const sum = techStackPercent.reduce((acc, val) => acc + val, 0);
|
const sum = techStackPercent.reduce((acc, val) => acc + val, 0);
|
||||||
const fullArr: [string, number, string, string, string][] = techStack.map((name, index) => [name, techStackPercent[index] / sum, techStackLightIconSVG.value[index] || '', techStackDarkIconSVG.value[index] || '', props.techStackThemeColors[index]] as [string, number, string, string, string]).sort((a, b) => b[1] - a[1]);
|
const fullArr: [string, number, string, string, string][] = techStack.map((name, index) => [name, techStackPercent[index]! / sum, techStackLightIconSVG.value[index] || '', techStackDarkIconSVG.value[index] || '', props.techStackThemeColors[index]] as [string, number, string, string, string]).sort((a, b) => b[1] - a[1]);
|
||||||
const dataArr: [string, number][] = fullArr.map((x) => [x[0], x[1]]);
|
const dataArr: [string, number][] = fullArr.map((x) => [x[0], x[1]]);
|
||||||
const barHeight = 20;
|
const barHeight = 20;
|
||||||
const gap = 10;
|
const gap = 10;
|
||||||
|
|
@ -83,8 +83,8 @@ const renderChart = () => {
|
||||||
labels: {
|
labels: {
|
||||||
useHTML: true,
|
useHTML: true,
|
||||||
formatter: function () {
|
formatter: function () {
|
||||||
return `<div style="width: 25px; height: 25px;" title="${fullArr[this.pos][0]}">
|
return `<div style="width: 25px; height: 25px;" title="${fullArr[this.pos]![0]}">
|
||||||
${colorMode.value === 'light' ? fullArr[this.pos][2] : fullArr[this.pos][3]}
|
${colorMode.value === 'light' ? fullArr[this.pos]![2] : fullArr[this.pos]![3]}
|
||||||
</div>`;
|
</div>`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -114,7 +114,7 @@ const renderChart = () => {
|
||||||
],
|
],
|
||||||
tooltip: {
|
tooltip: {
|
||||||
formatter: function () {
|
formatter: function () {
|
||||||
return `${fullArr[this.x][0]} ${toPercent(this.y)}`;
|
return `${fullArr[this.x]![0]} ${toPercent(this.y)}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
|
|
@ -216,7 +216,9 @@ const scrollbarOptions = {
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<overlay-scrollbars-component v-else class="max-h-full" :options="scrollbarOptions">
|
<overlay-scrollbars-component v-else class="max-h-full" :options="scrollbarOptions">
|
||||||
<div ref="chartRef" class="w-full"/>
|
<client-only>
|
||||||
|
<div ref="chartRef" class="w-full"/>
|
||||||
|
</client-only>
|
||||||
</overlay-scrollbars-component>
|
</overlay-scrollbars-component>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import useColorModeStore from '~/stores/colorModeStore';
|
||||||
import { useWindowScroll } from '@vueuse/core';
|
import { useWindowScroll } from '@vueuse/core';
|
||||||
|
|
||||||
const { colorMode } = storeToRefs(useColorModeStore());
|
const { colorMode } = storeToRefs(useColorModeStore());
|
||||||
|
watch(colorMode, () => {
|
||||||
|
console.log('colorMode changed:', colorMode.value);
|
||||||
|
});
|
||||||
const isHome = computed(() => useRoute().path === '/');
|
const isHome = computed(() => useRoute().path === '/');
|
||||||
const items = ref<NavigationMenuItem[]>([
|
const items = ref<NavigationMenuItem[]>([
|
||||||
{
|
{
|
||||||
|
|
@ -41,7 +44,6 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
const scrollY = useWindowScroll().y;
|
const scrollY = useWindowScroll().y;
|
||||||
const isScrollDown = ref(false);
|
const isScrollDown = ref(false);
|
||||||
// gsap.registerPlugin(ScrollTrigger);
|
|
||||||
|
|
||||||
watch(scrollY, (newY) => {
|
watch(scrollY, (newY) => {
|
||||||
if (newY > 0 && !collapsed.value) {
|
if (newY > 0 && !collapsed.value) {
|
||||||
|
|
@ -73,43 +75,45 @@ useRouter().afterEach(() => {
|
||||||
}"
|
}"
|
||||||
@mouseleave="collapsed = true">
|
@mouseleave="collapsed = true">
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<Transition
|
<client-only>
|
||||||
enter-active-class="transition-opacity duration-500 ease-in-out"
|
<Transition
|
||||||
enter-from-class="opacity-0"
|
enter-active-class="transition-opacity duration-500 ease-in-out"
|
||||||
enter-to-class="opacity-100"
|
enter-from-class="opacity-0"
|
||||||
leave-active-class="transition-opacity duration-500 ease-in-out"
|
enter-to-class="opacity-100"
|
||||||
leave-from-class="opacity-100"
|
leave-active-class="transition-opacity duration-500 ease-in-out"
|
||||||
leave-to-class="opacity-0"
|
leave-from-class="opacity-100"
|
||||||
>
|
leave-to-class="opacity-0"
|
||||||
<div
|
>
|
||||||
v-if="colorMode === 'light'"
|
|
||||||
class="flex h-full w-full absolute bg-[url('/79d52228c770808810a310115567e6790380823a.png')] bg-cover bg-top ">
|
|
||||||
<slot name="header"/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex h-full w-full absolute bg-[url('/anime-8788959.jpg')] bg-cover bg-center">
|
|
||||||
<slot name="header"/>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
<!-- header picture -->
|
|
||||||
<Transition
|
|
||||||
enter-active-class="transition-opacity duration-500 ease-in-out"
|
|
||||||
enter-from-class="opacity-0"
|
|
||||||
enter-to-class="opacity-100"
|
|
||||||
leave-active-class="transition-opacity duration-500 ease-in-out"
|
|
||||||
leave-from-class="opacity-100"
|
|
||||||
leave-to-class="opacity-0"
|
|
||||||
>
|
|
||||||
<div v-if="isScrollDown">
|
|
||||||
<div
|
<div
|
||||||
v-if="colorMode === 'light'"
|
v-if="colorMode === 'light'"
|
||||||
class="opacity-80 max-h-[48px] flex w-full h-full fixed bg-[url('/79d52228c770808810a310115567e6790380823a.png')] bg-cover bg-top"/>
|
class="flex h-full w-full absolute bg-[url('/79d52228c770808810a310115567e6790380823a.png')] bg-cover bg-top ">
|
||||||
|
<slot name="header"/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="opacity-20 max-h-[48px] flex w-full h-full fixed bg-[url('/anime-8788959.jpg')] bg-cover bg-center"/>
|
class="flex h-full w-full absolute bg-[url('/anime-8788959.jpg')] bg-cover bg-center">
|
||||||
</div>
|
<slot name="header"/>
|
||||||
</Transition>
|
</div>
|
||||||
|
</Transition>
|
||||||
|
<!-- header picture -->
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-opacity duration-500 ease-in-out"
|
||||||
|
enter-from-class="opacity-0"
|
||||||
|
enter-to-class="opacity-100"
|
||||||
|
leave-active-class="transition-opacity duration-500 ease-in-out"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<div v-if="isScrollDown">
|
||||||
|
<div
|
||||||
|
v-if="colorMode === 'light'"
|
||||||
|
class="opacity-80 max-h-[48px] flex w-full h-full fixed bg-[url('/79d52228c770808810a310115567e6790380823a.png')] bg-cover bg-top"/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="opacity-20 max-h-[48px] flex w-full h-full fixed bg-[url('/anime-8788959.jpg')] bg-cover bg-center"/>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</client-only>
|
||||||
<!-- navbar -->
|
<!-- navbar -->
|
||||||
<div
|
<div
|
||||||
class="fixed z-10 w-full transition-all duration-500 dark:bg-gray-800/60 bg-old-neutral-50/40 backdrop-blur-sm dark:backdrop-blur-md">
|
class="fixed z-10 w-full transition-all duration-500 dark:bg-gray-800/60 bg-old-neutral-50/40 backdrop-blur-sm dark:backdrop-blur-md">
|
||||||
|
|
@ -119,7 +123,13 @@ useRouter().afterEach(() => {
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="transition-all duration-500 flex 2xl:w-[1240px] xl:w-[1020px] lg:w-[964px] md:w-[708px] sm:w-[580px] w-10/12">
|
class="transition-all duration-500 flex 2xl:w-[1240px] xl:w-[1020px] lg:w-[964px] md:w-[708px] sm:w-[580px] w-10/12">
|
||||||
<UNavigationMenu :items="items" :class="colorMode" class="w-full"/>
|
<client-only>
|
||||||
|
<UNavigationMenu :items="items" :class="colorMode" class="w-full"/>
|
||||||
|
<template #fallback>
|
||||||
|
<!-- 骨架屏/占位内容 -->
|
||||||
|
<div class="w-full h-12 animate-pulse"></div>
|
||||||
|
</template>
|
||||||
|
</client-only>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<slot name="navbarRight" :is-scroll-down="isScrollDown"/>
|
<slot name="navbarRight" :is-scroll-down="isScrollDown"/>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: false,
|
|
||||||
compatibilityDate: '2025-05-15',
|
compatibilityDate: '2025-05-15',
|
||||||
devtools: { enabled: false },
|
devtools: { enabled: false },
|
||||||
vite: {
|
vite: {
|
||||||
|
|
@ -19,6 +18,7 @@ export default defineNuxtConfig({
|
||||||
css: ['~/assets/css/main.css'],
|
css: ['~/assets/css/main.css'],
|
||||||
ui: {
|
ui: {
|
||||||
colorMode: false,
|
colorMode: false,
|
||||||
|
fonts: false,
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
|
|
@ -32,8 +32,8 @@ export default defineNuxtConfig({
|
||||||
script: [{ src: '/darkVerify.js' }],
|
script: [{ src: '/darkVerify.js' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourcemap: {
|
// sourcemap: {
|
||||||
server: true,
|
// server: true,
|
||||||
client: true,
|
// client: true,
|
||||||
},
|
// },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
40
package.json
40
package.json
|
|
@ -10,38 +10,40 @@
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/content": "^3.6.3",
|
"@nuxt/content": "^3.7.1",
|
||||||
"@nuxt/eslint": "1.5.2",
|
"@nuxt/eslint": "1.9.0",
|
||||||
"@nuxt/icon": "^1.15.0",
|
"@nuxt/icon": "^2.0.0",
|
||||||
"@nuxt/ui": "3.2.0",
|
"@nuxt/ui": "4.0.0",
|
||||||
"@pinia/nuxt": "^0.11.1",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^13.9.0",
|
||||||
"better-sqlite3": "^12.2.0",
|
"better-sqlite3": "^12.4.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.36.0",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
"highcharts": "^12.3.0",
|
"highcharts": "^12.4.0",
|
||||||
"md-editor-v3": "^5.8.4",
|
"md-editor-v3": "^6.0.1",
|
||||||
"nuxt": "^3.17.6",
|
"nuxt": "^4.1.2",
|
||||||
"overlayscrollbars-vue": "^0.5.9",
|
"overlayscrollbars-vue": "^0.5.9",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
"tailwind-scrollbar": "^4.0.2",
|
"tailwind-scrollbar": "^4.0.2",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.21",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"word-count": "^0.3.1"
|
"word-count": "^0.3.1"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.15.0",
|
"packageManager": "pnpm@10.17.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/eslint-plugin": "^5.1.0",
|
"@iconify-json/lucide": "^1.2.68",
|
||||||
|
"@stylistic/eslint-plugin": "^5.4.0",
|
||||||
"@stylistic/eslint-plugin-jsx": "^4.4.1",
|
"@stylistic/eslint-plugin-jsx": "^4.4.1",
|
||||||
"@vue/eslint-config-typescript": "^14.6.0",
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
"eslint-plugin-vue": "^10.3.0",
|
"eslint-plugin-vue": "^10.5.0",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.4.0",
|
||||||
"less": "^4.4.0",
|
"less": "^4.4.1",
|
||||||
"overlayscrollbars": "^2.11.5",
|
"overlayscrollbars": "^2.12.0",
|
||||||
"typescript-eslint": "^8.35.1",
|
"typescript-eslint": "^8.44.1",
|
||||||
"vue-eslint-parser": "^10.2.0"
|
"vue-eslint-parser": "^10.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
|
import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
|
||||||
import type { PostMetaData } from '~/types/PostMetaData';
|
import type { PostMetaData } from '~/types/PostMetaData';
|
||||||
import breakpointsHelper from '~/utils/BreakpointsHelper';
|
import breakpointsHelper from '~/utils/BreakpointsHelper';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue';
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
metaData?: PostMetaData;
|
metaData?: PostMetaData;
|
||||||
|
|
@ -90,9 +91,9 @@ function getCostTime(length: number | DataAnomaly | undefined) {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 justify-between h-28">
|
<div class="flex mt-2 justify-between h-28">
|
||||||
<div class="overflow-y-auto">
|
<overlay-scrollbars-component>
|
||||||
{{ metaData?.description }}
|
{{ metaData?.description }}
|
||||||
</div>
|
</overlay-scrollbars-component>
|
||||||
<Transition
|
<Transition
|
||||||
enter-active-class="transition-opacity duration-500 ease-in-out"
|
enter-active-class="transition-opacity duration-500 ease-in-out"
|
||||||
enter-from-class="opacity-0"
|
enter-from-class="opacity-0"
|
||||||
|
|
@ -111,7 +112,6 @@ function getCostTime(length: number | DataAnomaly | undefined) {
|
||||||
class="min-w-64"
|
class="min-w-64"
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="flex mt-2">
|
<div class="flex mt-2">
|
||||||
|
|
@ -137,7 +137,6 @@ function getCostTime(length: number | DataAnomaly | undefined) {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoverContent>
|
</HoverContent>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="Array.isArray(metaData?.tags)" class="flex items-top">
|
<div v-if="Array.isArray(metaData?.tags)" class="flex items-top">
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,15 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
const { data: rawbody } = useAsyncData('simpleCard:' + props.metaData.id, async () => (await queryCollection('content').where('id', '=', props.metaData.id).first())?.rawbody);
|
const { data: rawbody } = useAsyncData('simpleCard:' + props.metaData.id, async () => (await queryCollection('content').where('id', '=', props.metaData.id).first())?.rawbody);
|
||||||
const collapsed = ref(true);
|
const collapsed = ref(true);
|
||||||
const typeChinese = new Map<string, string>([
|
const typeChinese = new Map<string | undefined, string>([
|
||||||
['rambling', '絮语'],
|
['rambling', '絮语'],
|
||||||
['announcement', '公告'],
|
['announcement', '公告'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function dateFormat(date: Date | DataAnomaly) {
|
function dateFormat(date: Date | DataAnomaly | undefined) {
|
||||||
|
if (!date) {
|
||||||
|
return 'date undefined';
|
||||||
|
}
|
||||||
if (date === DataAnomaly.DataNotFound || date === DataAnomaly.Invalid) {
|
if (date === DataAnomaly.DataNotFound || date === DataAnomaly.Invalid) {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +105,7 @@ onUnmounted(() => {
|
||||||
class="p-5 light:bg-old-neutral-200 dark:bg-old-neutral-800 min-h-64 transition-all duration-500"
|
class="p-5 light:bg-old-neutral-200 dark:bg-old-neutral-800 min-h-64 transition-all duration-500"
|
||||||
@click="reverseCollapsed">
|
@click="reverseCollapsed">
|
||||||
<div class="text-4xl">
|
<div class="text-4xl">
|
||||||
{{ (typeChinese.get(metaData.type) || 'unknown Type') + ':' }}{{ props.metaData.title }}
|
{{ (typeChinese.get(metaData?.type) || 'unknown Type') + ':' }}{{ props.metaData.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mt-2 max-w-[400px] overflow-hidden">
|
<div class="flex items-center mt-2 max-w-[400px] overflow-hidden">
|
||||||
|
|
||||||
|
|
@ -134,7 +137,7 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="metaData.isPinned" class="flex items-center ml-2">
|
<div v-if="metaData?.isPinned" class="flex items-center ml-2">
|
||||||
<Icon name="codicon:pinned"/>
|
<Icon name="codicon:pinned"/>
|
||||||
<div class="ml-1 text-nowrap">
|
<div class="ml-1 text-nowrap">
|
||||||
置顶
|
置顶
|
||||||
|
|
@ -168,7 +171,7 @@ onUnmounted(() => {
|
||||||
<HoverContent>
|
<HoverContent>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="ml-1">
|
<div class="ml-1">
|
||||||
{{ dateFormat(props.metaData.updated_at[props.metaData.updated_at.length - 1]) }}
|
{{ dateFormat(props.metaData?.updated_at[props.metaData.updated_at.length - 1]) }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #hoverContent>
|
<template #hoverContent>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -5,35 +5,39 @@ import type { PostMetaData } from '~/types/PostMetaData';
|
||||||
import SimpleCard from '~/pages/index/components/SimpleCard.vue';
|
import SimpleCard from '~/pages/index/components/SimpleCard.vue';
|
||||||
import ArticleCard from '~/pages/index/components/ArticleCard.vue';
|
import ArticleCard from '~/pages/index/components/ArticleCard.vue';
|
||||||
|
|
||||||
const { data: srcPostsMetaData } = useAsyncData(async () => sortMetaData((await queryCollection('content').all()).map((x) => toMetaDataType(x)), 'published_at', true));
|
const srcPostsMetaData = ref<PostMetaData[]>([]);
|
||||||
const postsMetaData = ref<PostMetaData[]>([]);
|
const postsMetaData = ref<PostMetaData[]>([]);
|
||||||
|
|
||||||
|
async function loadPostsMetaData() {
|
||||||
|
srcPostsMetaData.value = sortMetaData((await queryCollection('content').all()).map((x) => toMetaDataType(x)), 'published_at', true) || [];
|
||||||
|
postsMetaData.value = srcPostsMetaData.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadPostsMetaData();
|
||||||
|
|
||||||
function toArticlePage(article: PostMetaData) {
|
function toArticlePage(article: PostMetaData) {
|
||||||
navigateTo(`/article/${encodeURIComponent(article.id)}`);
|
navigateTo(`/article/${encodeURIComponent(article.id)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(srcPostsMetaData, () => {
|
watch(srcPostsMetaData, () => {
|
||||||
postsMetaData.value = srcPostsMetaData.value || [];
|
postsMetaData.value = srcPostsMetaData.value || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
function filterRuleChange(rule: (data: PostMetaData) => boolean) {
|
||||||
// async function loadMetaData() {
|
postsMetaData.value = (srcPostsMetaData.value || []).filter(rule);
|
||||||
//
|
|
||||||
// }
|
|
||||||
function filterRuleChange(rule: string) {
|
|
||||||
if (rule === '')
|
|
||||||
postsMetaData.value = srcPostsMetaData.value || [];
|
|
||||||
else
|
|
||||||
postsMetaData.value = (srcPostsMetaData.value || []).filter((post) => post.type === rule);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="table w-full mt-6 table-fixed">
|
<div class="table w-full mt-6 table-fixed">
|
||||||
<div class="sticky top-16 float-left bg-old-neutral-200 dark:bg-old-neutral-800 max-h-[calc(100vh-4rem)]">
|
<div class="sticky top-16 float-left max-h-[calc(100vh-4rem)]">
|
||||||
<div class="relative duration-500 transition-all xl:w-80 w-0 overflow-hidden">
|
<div class="relative duration-500 transition-all xl:w-80 w-0 overflow-hidden">
|
||||||
<div class="w-80 top-0 left-0 text-gray-800 dark:text-white p-5">
|
<div class="w-80 top-0 left-0 text-gray-800 dark:text-white">
|
||||||
<PersonalCard
|
<!-- <PersonalCard/>-->
|
||||||
v-if="postsMetaData" :posts-meta-data="postsMetaData!"
|
<ArticleDescriptionCards
|
||||||
|
v-if="postsMetaData"
|
||||||
|
class="mb-5" :posts-meta-data="srcPostsMetaData!"
|
||||||
@filter-rule-change="filterRuleChange"/>
|
@filter-rule-change="filterRuleChange"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
const getInitialMode = () => {
|
function getInitialMode(): 'light' | 'dark' {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// 优先用 html 的 class
|
// 优先用 html 的 class
|
||||||
if (document.documentElement.classList.contains('dark')) return 'dark';
|
if (document.documentElement.classList.contains('dark')) return 'dark';
|
||||||
if (document.documentElement.classList.contains('light')) return 'light';
|
if (document.documentElement.classList.contains('light')) return 'light';
|
||||||
// 其次用 localStorage
|
// 其次用 localStorage
|
||||||
return localStorage.getItem('system-theme-mode') || 'light';
|
const val = localStorage.getItem('system-theme-mode');
|
||||||
|
if (!!val || (val !== 'light' && val !== 'dark'))
|
||||||
|
return 'light';
|
||||||
|
return val;
|
||||||
}
|
}
|
||||||
return 'light'; // SSR 默认
|
return 'light'; // SSR 默认
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,6 @@ export default {
|
||||||
mode: 'jit',
|
mode: 'jit',
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
|
||||||
colors: {},
|
|
||||||
letterSpacing: {
|
|
||||||
doublewidest: '.2em',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
screens: breakpoints,
|
screens: breakpoints,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue