✨ 主页添加絮语卡片 归档部分初见雏形
This commit is contained in:
parent
1fb97e1cc2
commit
363eeb74dd
36
Feature.md
36
Feature.md
|
@ -1,36 +0,0 @@
|
|||
## 功能清单 / TODO
|
||||
- [ ] 主页/
|
||||
- [ ] 网站标题
|
||||
- [ ] 文章列表(时间排序)
|
||||
- [ ] 文章标题
|
||||
- [ ] 文章摘要
|
||||
- [ ] 文章标签
|
||||
- [ ] 个人信息展示
|
||||
- [ ] RSS 订阅
|
||||
- [ ] 登录管理页面
|
||||
- [ ] 文章详情页/post
|
||||
- [ ] 文章标题
|
||||
- [ ] 文章内容
|
||||
- [ ] 文章标签
|
||||
- [ ] 评论区 ??
|
||||
- [ ] 评论列表
|
||||
- [ ] 评论表单
|
||||
- [ ] 评论回复
|
||||
- [ ] 评论点赞
|
||||
- [ ] 评论删除
|
||||
- [ ] 上一篇/下一篇文章链接
|
||||
- [ ] 归档页/archive
|
||||
- [ ] 按时间归档
|
||||
- [ ] 按标签归档
|
||||
- [ ] 搜索
|
||||
- [ ] 友链/friend
|
||||
- [ ] 关于
|
||||
- [ ] 后台管理/admin security??
|
||||
- [ ] 文章编辑
|
||||
- [ ] 新建文章
|
||||
- [ ] 编辑文章
|
||||
- [ ] 删除文章
|
||||
- [ ] 文章预览
|
||||
- [ ] markdown mermaid ... 支持
|
||||
- [ ] 友链管理
|
||||
- [ ] 个人页面管理
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<script setup lang="ts">
|
||||
import { MdPreview } from 'md-editor-v3';
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import useColorModeStore from '~/stores/colorModeStore';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
editorId: string;
|
||||
markdown?: string;
|
||||
}>(), {
|
||||
markdown: () => '## Hello World!',
|
||||
});
|
||||
console.log(props.markdown);
|
||||
const eraseHeaderMarkdown = computed(() => props.markdown.replace(/^---[\s\S]*?---\n?/, ''));
|
||||
|
||||
const { colorMode } = storeToRefs(useColorModeStore());
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5 pt-0 bg-old-neutral-200 dark:bg-old-neutral-800">
|
||||
<MdPreview :editor-id="editorId" :theme="colorMode" :model-value="eraseHeaderMarkdown" class="transition-all duration-500"/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(.md-editor) {
|
||||
--md-bk-color: #e5e5e5;
|
||||
--md-theme-heading-1-color: #fff;
|
||||
transition-property: all;
|
||||
transition-duration: 500ms;
|
||||
--tw-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
}
|
||||
|
||||
:global(.dark .md-editor) {
|
||||
--md-bk-color: #262626;
|
||||
--md-theme-heading-1-color: #fff;
|
||||
}
|
||||
:global(.dark .md-editor-preview) {
|
||||
--md-color: var(--ui-text);
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import Highcharts from 'highcharts';
|
||||
import { DataAnomaly } from '~/types/ArticleMetaData';
|
||||
import { DataAnomaly } from '~/types/PostMetaData';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
import useColorModeStore from '~/stores/colorModeStore';
|
||||
import useIconStore from '~/stores/iconStore';
|
||||
|
@ -9,11 +9,11 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue';
|
|||
import type { OverflowBehavior, ScrollbarsAutoHideBehavior, ScrollbarsVisibilityBehavior } from 'overlayscrollbars';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
asyncKey: string;
|
||||
techStack?: string[] | DataAnomaly;
|
||||
techStackPercent?: number[] | DataAnomaly;
|
||||
techStackIconNames?: string[] | DataAnomaly;
|
||||
techStackThemeColors?: string[] | DataAnomaly;
|
||||
asyncKey: string;
|
||||
}>(), {
|
||||
techStack: () => ['Vue', 'Nuxt', 'TypeScript', 'JavaScript', 'CSS', 'HTML', 'Node.js', 'Python', 'Java', 'C#'],
|
||||
techStackPercent: () => [5, 3, 4, 2, 1, 1, 2, 3, 2, 1],
|
||||
|
@ -26,6 +26,7 @@ const techStackLightIconSVG = ref<string[]>([]);
|
|||
const techStackDarkIconSVG = ref<string[]>([]);
|
||||
const { colorMode } = storeToRefs(useColorModeStore());
|
||||
|
||||
// 没有这个asyncKey会加载不出正确的图标
|
||||
const { pending } = useAsyncData(props.asyncKey, async () => {
|
||||
if (props.techStackIconNames === DataAnomaly.DataNotFound || props.techStackIconNames === DataAnomaly.Invalid)
|
||||
return [];
|
||||
|
@ -147,7 +148,7 @@ onMounted(() => {
|
|||
noDataAvailable.value = true;
|
||||
return;
|
||||
}
|
||||
window.addEventListener('resize', renderChart);
|
||||
// window.addEventListener('resize', renderChart);
|
||||
colorModeCallBackKey = useColorModeStore().registerCallBack(renderChart);
|
||||
watch(pending, (isPending) => {
|
||||
if (!isPending) {
|
||||
|
@ -158,7 +159,7 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', renderChart);
|
||||
// window.removeEventListener('resize', renderChart);
|
||||
useColorModeStore().unregisterCallBack(colorModeCallBackKey);
|
||||
});
|
||||
|
||||
|
@ -184,17 +185,33 @@ const scrollbarOptions = {
|
|||
<div v-if="noDataAvailable" class="flex items-center justify-center h-full p-8">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 112.01">
|
||||
<g id="_图层_1" data-name="图层 1">
|
||||
<polyline class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" points="1.5 28.63 1.5 1.5 63.5 1.59"/>
|
||||
<polyline class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" points="254.5 83.38 254.5 110.5 192.5 110.41"/>
|
||||
<polyline class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10] stroke-3" points="254.5 28.63 254.5 1.5 192.5 1.59"/>
|
||||
<polyline class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10] stroke-3" points="1.5 83.38 1.5 110.5 63.5 110.41"/>
|
||||
<polyline
|
||||
class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]"
|
||||
points="1.5 28.63 1.5 1.5 63.5 1.59"/>
|
||||
<polyline
|
||||
class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]"
|
||||
points="254.5 83.38 254.5 110.5 192.5 110.41"/>
|
||||
<polyline
|
||||
class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10] stroke-3"
|
||||
points="254.5 28.63 254.5 1.5 192.5 1.59"/>
|
||||
<polyline
|
||||
class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10] stroke-3"
|
||||
points="1.5 83.38 1.5 110.5 63.5 110.41"/>
|
||||
</g>
|
||||
<g id="_图层_2" data-name="图层 2">
|
||||
<text class="[font-family:ArialMT,Arial] text-xl stroke-old-neutral-600 dark:stroke-old-neutral-400 fill-old-neutral-600 dark:fill-old-neutral-400" transform="translate(89.48 61.61)"><tspan x="0" y="0">No Data</tspan></text>
|
||||
<text
|
||||
class="[font-family:ArialMT,Arial] text-xl stroke-old-neutral-600 dark:stroke-old-neutral-400 fill-old-neutral-600 dark:fill-old-neutral-400"
|
||||
transform="translate(89.48 61.61)">
|
||||
<tspan x="0" y="0">No Data</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g id="_图层_3" data-name="图层 3">
|
||||
<line class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" x1="16" y1="11" x2="90" y2="39.8"/>
|
||||
<line class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" x1="166" y1="72.21" x2="240" y2="101"/>
|
||||
<line
|
||||
class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" x1="16"
|
||||
y1="11" x2="90" y2="39.8"/>
|
||||
<line
|
||||
class="fill-none stroke-old-neutral-400 dark:stroke-old-neutral-500 [stroke-miterlimit:10]" x1="166"
|
||||
y1="72.21" x2="240" y2="101"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
2
content
2
content
|
@ -1 +1 @@
|
|||
Subproject commit 58eedf0bd1299ee92b64d0cc97f2f884a2b77ad2
|
||||
Subproject commit a7cd0a03602ed93da797632e5616211f40868125
|
|
@ -1,25 +1,29 @@
|
|||
import { defineContentConfig, defineCollection, z } from '@nuxt/content';
|
||||
|
||||
const schema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
category: z.string().optional(),
|
||||
created_at: z.string().datetime(),
|
||||
published_at: z.string().datetime().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
updated_at: z.array(z.string().datetime()).default([]),
|
||||
tags: z.array(z.string()).default([]),
|
||||
type: z.enum(['article', 'rambling']).default('article'),
|
||||
tech_stack: z.array(z.string()).default([]),
|
||||
tech_stack_percent: z.array(z.number()).default([]),
|
||||
tech_stack_icon_names: z.array(z.string()).default([]),
|
||||
tech_stack_theme_colors: z.array(z.string()).default([]),
|
||||
rawbody: z.string(),
|
||||
});
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
content: defineCollection({
|
||||
type: 'page',
|
||||
source: '**/*.md',
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
created_at: z.string().datetime(),
|
||||
published_at: z.string().datetime().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
updated_at: z.array(z.string().datetime()).default([]),
|
||||
tags: z.array(z.string()).default([]),
|
||||
tech_stack: z.array(z.string()).default([]),
|
||||
tech_stack_percent: z.array(z.number()).default([]),
|
||||
tech_stack_icon_names: z.array(z.string()).default([]),
|
||||
tech_stack_theme_colors: z.array(z.string()).default([]),
|
||||
rawbody: z.string(),
|
||||
}),
|
||||
schema: schema,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||
import useColorModeStore from '~/stores/colorModeStore';
|
||||
import { useWindowScroll } from '@vueuse/core';
|
||||
// import gsap from 'gsap';
|
||||
// import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
const { colorMode } = storeToRefs(useColorModeStore());
|
||||
const isHome = computed(() => useRoute().path === '/');
|
||||
|
@ -42,18 +40,25 @@ onMounted(() => {
|
|||
}, 2000);
|
||||
});
|
||||
const scrollY = useWindowScroll().y;
|
||||
const headerHeight = ref(0);
|
||||
const isScrollDown = ref(false);
|
||||
// gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
watch(scrollY, (newY) => {
|
||||
if (newY > 0 && !collapsed.value) {
|
||||
collapsed.value = true;
|
||||
headerHeight.value = 0;
|
||||
}
|
||||
isScrollDown.value = newY > 215;
|
||||
});
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
||||
useRouter().beforeEach(() => {
|
||||
isLoading.value = true;
|
||||
});
|
||||
|
||||
useRouter().afterEach(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -78,12 +83,12 @@ watch(scrollY, (newY) => {
|
|||
>
|
||||
<div
|
||||
v-if="colorMode === 'light'"
|
||||
class="flex w-full h-full absolute 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
|
||||
v-else
|
||||
class="flex w-full h-full absolute 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">
|
||||
<slot name="header"/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
@ -107,25 +112,35 @@ watch(scrollY, (newY) => {
|
|||
</Transition>
|
||||
<!-- navbar -->
|
||||
<div
|
||||
class="fixed z-10 w-full flex justify-center 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">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="not-xl:hidden">
|
||||
<slot name="navbarLeft" :is-scroll-down="isScrollDown"/>
|
||||
</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-[400px]">
|
||||
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"/>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<slot name="navbarRight" :is-scroll-down="isScrollDown"/>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<UProgress v-if="isLoading" :size="'sm'" class="fixed z-20"/>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex justify-center items-center duration-500 bg-white dark:bg-[#16191b] h-full">
|
||||
<div
|
||||
:class="collapsed ? 'min-h-[80vh]' : 'min-h-[60vh]'"
|
||||
class="transition-all duration-500 ease-in-out 2xl:w-[1240px] xl:w-[1020px] lg:w-[964px] md:w-[708px] sm:w-[580px] w-[400px]">
|
||||
class="transition-all duration-500 ease-in-out 2xl:w-[1240px] xl:w-[1020px] lg:w-[964px] md:w-[708px] sm:w-[580px] w-11/12">
|
||||
<slot name="content"/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -22,11 +22,14 @@ export default defineNuxtConfig({
|
|||
},
|
||||
app: {
|
||||
head: {
|
||||
htmlAttrs: {
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
script: [{ src: '/darkVerify.js' }],
|
||||
},
|
||||
},
|
||||
sourcemap: {
|
||||
server: true,
|
||||
client: true
|
||||
}
|
||||
client: true,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"eslint": "^9.0.0",
|
||||
"gsap": "^3.13.0",
|
||||
"highcharts": "^12.3.0",
|
||||
"md-editor-v3": "^5.8.4",
|
||||
"nuxt": "^3.17.6",
|
||||
"overlayscrollbars-vue": "^0.5.9",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
|
@ -39,6 +40,7 @@
|
|||
"eslint-plugin-vue": "^10.3.0",
|
||||
"globals": "^16.3.0",
|
||||
"less": "^4.4.0",
|
||||
"overlayscrollbars": "^2.11.5",
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"vue-eslint-parser": "^10.2.0"
|
||||
}
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
// onMounted(() =>
|
||||
// useColorMode().preference = 'dark',
|
||||
// );
|
||||
// function clickHandler() {
|
||||
// console.log('Button clicked');
|
||||
// useColorMode().preference = useColorMode().preference === 'light'? 'dark' : 'light';
|
||||
// }
|
||||
|
||||
import useColorModeStore from '~/stores/colorModeStore';
|
||||
import breakpointsHelper from '~/utils/BreakpointsHelper';
|
||||
|
||||
// const { colorMode } = storeToRefs(useColorModeStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -23,7 +15,9 @@ import useColorModeStore from '~/stores/colorModeStore';
|
|||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-if="isScrollDown" class="pl-5 text-xl h-12 leading-11 flex">
|
||||
<div
|
||||
v-if="isScrollDown && breakpointsHelper.greaterOrEqual('xl').value"
|
||||
class="pl-5 text-xl h-12 leading-11 flex">
|
||||
随机存取
|
||||
</div>
|
||||
</Transition>
|
||||
|
@ -61,7 +55,7 @@ import useColorModeStore from '~/stores/colorModeStore';
|
|||
</template>
|
||||
<template #header>
|
||||
<div class="w-full flex-1 justify-center flex items-center">
|
||||
<p class="text-8xl">
|
||||
<p class="text-8xl mt-12">
|
||||
随机存取
|
||||
</p>
|
||||
</div>
|
||||
|
@ -70,17 +64,17 @@ import useColorModeStore from '~/stores/colorModeStore';
|
|||
<div>
|
||||
<NuxtRouteAnnouncer/>
|
||||
<NuxtPage/>
|
||||
<button
|
||||
@click="() => {
|
||||
useColorModeStore().toggleColorMode();
|
||||
}
|
||||
">swap Theme
|
||||
</button>
|
||||
<div class="min-h-[100vh]"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="w-full flex-1 justify-center flex items-center"></div>
|
||||
<div class="w-full flex flex-col justify-center items-center p-10 text-old-neutral-500">
|
||||
<div>
|
||||
© 2025 随机存取. 由Lichx制作
|
||||
</div>
|
||||
<div>
|
||||
蒙ICP备2025022865号
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<script setup lang="ts">
|
||||
import { DataAnomaly } from '~/types/PostMetaData';
|
||||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
|
||||
const articles = defineModel<PostMetaData[]>('articles', {
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
currentChoice?: 'time' | 'category';
|
||||
}>(),
|
||||
{
|
||||
currentChoice: 'time',
|
||||
});
|
||||
|
||||
watch(() => props.currentChoice, (newChoice) => {
|
||||
if (newChoice === 'time') {
|
||||
articles.value.sort((a, b) => new Date(b.published_at || '2000-01-01').getTime() - new Date(a.published_at || '2000-01-01').getTime());
|
||||
} else {
|
||||
articles.value.sort((a, b) => {
|
||||
if (a.category === b.category) {
|
||||
return new Date(b.published_at || '2000-01-01').getTime() - new Date(a.published_at || '2000-01-01').getTime();
|
||||
}
|
||||
return a.category.localeCompare(b.category);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function toArticlePage(article: PostMetaData) {
|
||||
navigateTo(`/article/${encodeURIComponent(article.id)}`);
|
||||
}
|
||||
|
||||
function getYear(article: PostMetaData) {
|
||||
return new Date(article.published_at).getFullYear();
|
||||
}
|
||||
|
||||
function dateFormatToTime(date: Date | DataAnomaly) {
|
||||
if (date === DataAnomaly.DataNotFound || date === DataAnomaly.Invalid) {
|
||||
return date;
|
||||
}
|
||||
return new Date(date).toLocaleDateString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function dateFormatToDate(date: Date | DataAnomaly) {
|
||||
if (date === DataAnomaly.DataNotFound || date === DataAnomaly.Invalid) {
|
||||
return date;
|
||||
}
|
||||
return new Date(date).toLocaleDateString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).replace(/\//g, '-');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="(article,index) of articles" :key="article.id"
|
||||
class="border-l-2 border-l-old-neutral-400 dark:border-l-old-neutral-500 pl-4">
|
||||
<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="currentChoice==='time' && index == 0 || getYear(article) != getYear(articles[index-1])"
|
||||
class="year-marker relative text-indigo-300 text-2xl pt-3 pb-3">
|
||||
{{ getYear(article) }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="currentChoice==='category' && (index == 0 || article.category != articles[index-1].category)"
|
||||
class="year-marker relative text-indigo-300 text-2xl pt-3 pb-3">
|
||||
{{ article.category }}
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="flex items-center" @click="toArticlePage(article)">
|
||||
<div :title="dateFormatToTime(article.published_at)" class="text-sm w-12">
|
||||
{{ dateFormatToDate(article.published_at) }}
|
||||
</div>
|
||||
<div :title="dateFormatToTime(article.published_at)" class="text-md pl-5">
|
||||
{{ article.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.year-marker::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12px; // w-3
|
||||
height: 12px; // h-3
|
||||
background-color: #000;
|
||||
border-radius: 9999px; // rounded-full
|
||||
border: 2px solid black; // border-2 border-black
|
||||
left: -17px; // pl-3 12px + 1px border
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
transition: background-color 0.5s, border-color 0.5s;
|
||||
}
|
||||
|
||||
.dark .year-marker::before {
|
||||
background-color: #fff;
|
||||
border: 2px solid white; // border-2 border-white
|
||||
}
|
||||
</style>
|
|
@ -1,9 +1,37 @@
|
|||
<script setup lang="ts">
|
||||
import { toMetaDataType } from '~/types/PostMetaData';
|
||||
import TimeLine from '~/pages/index/archive/components/TimeLine.vue';
|
||||
|
||||
const { data: articles } = useAsyncData(async () => (await queryCollection('content').order('published_at', 'DESC').all()).map((article) => toMetaDataType(article)));
|
||||
|
||||
const currentChoice = ref('时间');
|
||||
|
||||
// onMounted(() => {
|
||||
// setTimeout(() => {
|
||||
// console.log(articles.value);
|
||||
// }, 2000);
|
||||
// });
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<div>
|
||||
<div class="table w-full mt-6">
|
||||
<div class="sticky top-16 float-left bg-old-neutral-200 dark:bg-old-neutral-800 max-h-[calc(100vh-4rem)]">
|
||||
<div class="relative duration-500 transition-all xl:w-80 w-0 mr-2/3 overflow-hidden">
|
||||
<div class="w-80 top-0 left-0 text-gray-800 dark:text-white p-5">
|
||||
test123456
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="transition-all duration-500 float-right xl:w-[calc(100%-20rem-40px)] w-full bg-old-neutral-200 dark:bg-old-neutral-800 p-5">
|
||||
<URadioGroup
|
||||
v-model="currentChoice" orientation="horizontal" variant="table" :items="['时间', '类别']" size="sm"
|
||||
class="mb-5"/>
|
||||
<TimeLine v-if="articles" v-model:articles="articles!" :current-choice="currentChoice=== '时间'? 'time' : 'category'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
|
||||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
import breakpointsHelper from '~/utils/BreakpointsHelper';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
article?: PostMetaData;
|
||||
}>(), {
|
||||
article: () => defaultMetaData,
|
||||
});
|
||||
|
||||
function dateFormat(date: Date | DataAnomaly) {
|
||||
if (date === DataAnomaly.DataNotFound || date === DataAnomaly.Invalid) {
|
||||
return date;
|
||||
}
|
||||
return new Date(date).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function getCostTime(length: number | DataAnomaly) {
|
||||
if (length === DataAnomaly.DataNotFound || length === DataAnomaly.Invalid) {
|
||||
return length;
|
||||
}
|
||||
let time = length / 250;
|
||||
time = Math.ceil(time);
|
||||
const hours = Math.floor(time / 60);
|
||||
const minutes = time % 60;
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`;
|
||||
} else {
|
||||
return `${minutes}分钟`;
|
||||
}
|
||||
}
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
open.value = true;
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
open.value = false;
|
||||
}, 4000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 pb-0 light:bg-old-neutral-200 dark:bg-old-neutral-800">
|
||||
<UCollapsible v-model:open="open" :unmount-on-hide="false" class="flex flex-col gap-2 w-full">
|
||||
<div class="text-4xl flex justify-between items-center w-full">
|
||||
<div class="mb-0 pb-0">
|
||||
{{ props.article.title }}
|
||||
</div>
|
||||
<Icon
|
||||
name="lucide:chevron-down" class="text-2xl transition-transform duration-300 mr-5"
|
||||
:class="{ 'rotate-180': open }"/>
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="flex mt-2 justify-between">
|
||||
<div>
|
||||
<div class="flex items-center mt-2 max-w-96 overflow-hidden">
|
||||
<div title="发布时间" class="flex items-center">
|
||||
<Icon name="lucide:clock-arrow-up"/>
|
||||
<div class="ml-1">
|
||||
{{ dateFormat(props.article.published_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="分类" class="flex items-center ml-2">
|
||||
<Icon name="material-symbols:category"/>
|
||||
<div class="ml-1 inline">
|
||||
{{ props.article.category }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex items-center max-w-96 overflow-hidden">
|
||||
<div title="字数" class="flex items-center">
|
||||
<Icon name="fluent:text-word-count-20-filled"/>
|
||||
<div class="ml-1 inline">
|
||||
{{ props.article.word_count }}字
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="预计阅读时间" class="flex items-center ml-2">
|
||||
<Icon name="octicon:stopwatch-16"/>
|
||||
<div class="ml-1 inline">
|
||||
{{ getCostTime(props.article.word_count) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<div title="创建时间" class="flex items-center">
|
||||
<Icon name="lucide:file-clock"/>
|
||||
<div class="ml-1">{{ dateFormat(props.article.created_at) }}</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.article.updated_at)" class="flex items-center ml-2">
|
||||
<Icon name="lucide:clock-alert" title="上次更新时间"/>
|
||||
<HoverContent>
|
||||
<template #content>
|
||||
<div class="ml-1">
|
||||
{{ dateFormat(props.article.updated_at[props.article.updated_at.length - 1]) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #hoverContent>
|
||||
<div class="p-1 pr-2">
|
||||
<div v-for="(date,index) of props.article.updated_at" :key="index">
|
||||
<div class="block whitespace-nowrap">
|
||||
{{ '第' + index + '次更新' + dateFormat(date) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoverContent>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.article.tags)" class="flex items-center">
|
||||
<Icon name="clarity:tags-solid"/>
|
||||
<div v-for="(tag,index) of props.article.tags" :key="index">
|
||||
<div class="ml-1">{{ tag }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
<TechStackCard
|
||||
v-if="breakpointsHelper.greater('lg').value"
|
||||
:async-key="'stack:' + props.article.id"
|
||||
:tech-stack="props.article.tech_stack"
|
||||
:tech-stack-icon-names="props.article.tech_stack_icon_names"
|
||||
:tech-stack-theme-colors="props.article.tech_stack_theme_colors"
|
||||
:tech-stack-percent="props.article.tech_stack_percent"
|
||||
class="w-64"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</UCollapsible>
|
||||
</div>
|
||||
</template>
|
|
@ -1,14 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { toMetaDataType } from '~/types/PostMetaData';
|
||||
import { MdCatalog } from 'md-editor-v3';
|
||||
import ArticleHeader from '~/pages/index/article/[articleID]/components/ArticleHeader.vue';
|
||||
|
||||
const articleId = useRoute().params.articleID as string;
|
||||
const { data: article } = useAsyncData(async () => await queryCollection('content').where('id', '=', articleId).first());
|
||||
|
||||
const editorId = 'article-previewer';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{useRoute().params.articleID}}
|
||||
<div class="table w-full mt-6">
|
||||
<div class="sticky top-16 float-left bg-old-neutral-200 dark:bg-old-neutral-800 max-h-[calc(100vh-4rem)]">
|
||||
<div class="relative duration-500 transition-all xl:w-80 w-0 mr-2/3 overflow-hidden">
|
||||
<div class="w-80 top-0 left-0 text-gray-800 dark:text-white p-5">
|
||||
<MdCatalog :editor-id="editorId" :scroll-element="'html'"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="transition-all duration-500 float-right xl:w-[calc(100%-20rem-40px)] w-full">
|
||||
<ArticleHeader v-if="article" class="w-full" :article="toMetaDataType(article)"/>
|
||||
<!-- <ArticleCard v-if="article" class=" w-full" :article="toArticleMetaDataType(article)"/>-->
|
||||
<ReadonlyMdEditor :editor-id="editorId" :markdown="article?.rawbody"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="less">
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
Article
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,34 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { DataAnomaly, toArticleMetaDataType } from '~/types/ArticleMetaData';
|
||||
import type { ArticleMetaData } from '~/types/ArticleMetaData';
|
||||
import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
|
||||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
import breakpointsHelper from '~/utils/BreakpointsHelper';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
article?: ArticleMetaData;
|
||||
article?: PostMetaData;
|
||||
}>(),
|
||||
{
|
||||
article: () => toArticleMetaDataType({
|
||||
id: 'default ID',
|
||||
title: 'ApiFox / Postman 使用WebSocket连接SignalR进行测试需要注意的小问题',
|
||||
description: 'default Description',
|
||||
created_at: new Date('2025-01-01T00:00:00Z'), // 默认创建时间
|
||||
published_at: new Date('2025-01-01T00:01:00Z'), // 默认发布时间
|
||||
draft: true,
|
||||
updated_at: [new Date('2025-01-01T00:02:00Z'), new Date('2025-01-01T00:03:00Z')], // 默认更新时间
|
||||
tags: ['C#', 'TS', 'Windows Professional version with Webstorm 2025'],
|
||||
tech_stack: new Map([
|
||||
['Vue', 5],
|
||||
['Nuxt', 3],
|
||||
['TypeScript', 4],
|
||||
['JavaScript', 2],
|
||||
['CSS', 1],
|
||||
['HTML', 1],
|
||||
['Node.js', 2],
|
||||
['Python', 3],
|
||||
['Java', 2],
|
||||
['C#', 1],
|
||||
]),
|
||||
}),
|
||||
article: () => defaultMetaData,
|
||||
});
|
||||
|
||||
function dateFormat(date: Date | DataAnomaly) {
|
||||
|
@ -44,9 +24,6 @@ function dateFormat(date: Date | DataAnomaly) {
|
|||
});
|
||||
}
|
||||
|
||||
// const { colorMode } = storeToRefs(useColorModeStore());
|
||||
// console.log(props.article.tech_stack)
|
||||
|
||||
function getCostTime(length: number | DataAnomaly) {
|
||||
if (length === DataAnomaly.DataNotFound || length === DataAnomaly.Invalid) {
|
||||
return length;
|
||||
|
@ -64,29 +41,36 @@ function getCostTime(length: number | DataAnomaly) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5 light:bg-old-neutral-200 dark:bg-old-neutral-800 min-h-64">
|
||||
<div class="p-5 light:bg-old-neutral-200 dark:bg-old-neutral-800 min-h-64 transition-all duration-500">
|
||||
<div class="text-4xl">
|
||||
{{ props.article.title }}
|
||||
</div>
|
||||
<div class="flex items-center mt-2">
|
||||
<div class="flex items-center mt-2 max-w-96 overflow-hidden">
|
||||
|
||||
<div title="发布时间" class="flex items-center">
|
||||
<Icon name="lucide:clock-arrow-up"/>
|
||||
<div class="ml-1">
|
||||
{{ dateFormat(props.article.published_at) }}
|
||||
<div class="ml-1 text-nowrap">
|
||||
{{ dateFormat(props.article.published_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="分类" class="flex items-center ml-2">
|
||||
<Icon name="material-symbols:category"/>
|
||||
<div class="ml-1 text-nowrap">
|
||||
{{ props.article.category }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="字数" class="flex items-center ml-2">
|
||||
<Icon name="fluent:text-word-count-20-filled"/>
|
||||
<div class="ml-1 inline">
|
||||
<div class="ml-1 text-nowrap">
|
||||
{{ props.article.word_count }}字
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="预计阅读时间" class="flex items-center ml-2">
|
||||
<Icon name="octicon:stopwatch-16"/>
|
||||
<div class="ml-1 inline">
|
||||
<div class="ml-1 text-nowrap">
|
||||
{{ getCostTime(props.article.word_count) }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -98,14 +82,25 @@ function getCostTime(length: number | DataAnomaly) {
|
|||
{{ props.article.description }}
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
<TechStackCard
|
||||
:async-key="props.article.id"
|
||||
v-if="breakpointsHelper.greater('lg').value"
|
||||
:async-key="'stack:' + props.article.id"
|
||||
:tech-stack="props.article.tech_stack"
|
||||
:tech-stack-icon-names="props.article.tech_stack_icon_names"
|
||||
:tech-stack-theme-colors="props.article.tech_stack_theme_colors"
|
||||
:tech-stack-percent="props.article.tech_stack_percent"
|
||||
class="w-64"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="flex mt-2">
|
||||
|
@ -134,15 +129,14 @@ function getCostTime(length: number | DataAnomaly) {
|
|||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.article.tags)" class="flex items-center">
|
||||
<Icon name="clarity:tags-solid"/>
|
||||
<div v-for="(tag,index) of props.article.tags" :key="index">
|
||||
<div class="ml-1">{{ tag }}</div>
|
||||
<div v-if="Array.isArray(props.article.tags)" class="flex items-top">
|
||||
<Icon name="clarity:tags-solid" class="mt-1"/>
|
||||
<div>
|
||||
<div v-for="(tag,index) of props.article.tags" :key="index" class="inline-block">
|
||||
<div class="ml-1 inline">{{ tag }}</div>
|
||||
<div v-if="index !== props.article.tags.length - 1" class="inline">,</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,176 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
|
||||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
import useColorModeStore from '~/stores/colorModeStore';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
rambling?: PostMetaData;
|
||||
}>(),
|
||||
{
|
||||
rambling: () => defaultMetaData,
|
||||
});
|
||||
const { data: rawbody } = useAsyncData(async () => (await queryCollection('content').where('id', '=', props.rambling.id).first())?.rawbody);
|
||||
const collapsed = ref(false);
|
||||
|
||||
function dateFormat(date: Date | DataAnomaly) {
|
||||
if (date === DataAnomaly.DataNotFound || date === DataAnomaly.Invalid) {
|
||||
return date;
|
||||
}
|
||||
return new Date(date).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function getCostTime(length: number | DataAnomaly) {
|
||||
if (length === DataAnomaly.DataNotFound || length === DataAnomaly.Invalid) {
|
||||
return length;
|
||||
}
|
||||
let time = length / 250;
|
||||
time = Math.ceil(time);
|
||||
const hours = Math.floor(time / 60);
|
||||
const minutes = time % 60;
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`;
|
||||
} else {
|
||||
return `${minutes}分钟`;
|
||||
}
|
||||
}
|
||||
|
||||
const safeEditorId = computed(() => {
|
||||
const encoded = btoa(encodeURIComponent(props.rambling.id))
|
||||
.replace(/[+/=]/g, '_'); // 替换 Base64 中的特殊字符
|
||||
return `rambling_${encoded}`;
|
||||
});
|
||||
|
||||
const showLightShadow = ref(false);
|
||||
const showDarkShadow = ref(false);
|
||||
|
||||
function reverseCollapsed() {
|
||||
if (collapsed.value) {
|
||||
collapsed.value = false;
|
||||
return;
|
||||
}
|
||||
const showLight = showLightShadow.value;
|
||||
const showDark = showDarkShadow.value;
|
||||
showLightShadow.value = false;
|
||||
showDarkShadow.value = false;
|
||||
collapsed.value = true;
|
||||
setTimeout(() => {
|
||||
showLightShadow.value = showLight;
|
||||
showDarkShadow.value = showDark;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
const colorModeStore = useColorModeStore();
|
||||
let colorModeCallBackKey = '';
|
||||
onMounted(() => {
|
||||
if (colorModeStore.colorMode === 'light') {
|
||||
showLightShadow.value = true;
|
||||
} else {
|
||||
showDarkShadow.value = true;
|
||||
}
|
||||
colorModeCallBackKey = colorModeStore.registerCallBack(() => {
|
||||
if (colorModeStore.colorMode === 'light') {
|
||||
setTimeout(() => {
|
||||
showLightShadow.value = true;
|
||||
}, 500);
|
||||
showDarkShadow.value = false;
|
||||
} else {
|
||||
showLightShadow.value = false;
|
||||
setTimeout(() => {
|
||||
showDarkShadow.value = true;
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
onUnmounted(() => {
|
||||
colorModeStore.unregisterCallBack(colorModeCallBackKey);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="p-5 light:bg-old-neutral-200 dark:bg-old-neutral-800 min-h-64 transition-all duration-500"
|
||||
@click="reverseCollapsed">
|
||||
<div class="text-4xl">
|
||||
絮语:{{ props.rambling.title }}
|
||||
</div>
|
||||
<div class="flex items-center mt-2 max-w-96 overflow-hidden">
|
||||
|
||||
<div title="发布时间" class="flex items-center">
|
||||
<Icon name="lucide:clock-arrow-up"/>
|
||||
<div class="ml-1 text-nowrap">
|
||||
{{ dateFormat(props.rambling.published_at) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="分类" class="flex items-center ml-2">
|
||||
<Icon name="material-symbols:category"/>
|
||||
<div class="ml-1 text-nowrap">
|
||||
{{ props.rambling.category }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="字数" class="flex items-center ml-2">
|
||||
<Icon name="fluent:text-word-count-20-filled"/>
|
||||
<div class="ml-1 text-nowrap">
|
||||
{{ props.rambling.word_count }}字
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="预计阅读时间" class="flex items-center ml-2">
|
||||
<Icon name="octicon:stopwatch-16"/>
|
||||
<div class="ml-1 text-nowrap">
|
||||
{{ getCostTime(props.rambling.word_count) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="relative flex mt-2 justify-between overflow-hidden transition-all duration-300 ease-in-out dura"
|
||||
:class="{'max-h-[8.5rem]' : collapsed, 'max-h-[100vh]':!collapsed}">
|
||||
<ReadonlyMdEditor
|
||||
v-if="rawbody" :editor-id="safeEditorId" :markdown="rawbody!"
|
||||
class="transition-all duration-500 w-full"/>
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 h-14 bg-gradient-to-t from-old-neutral-200 to-transparent pointer-events-none transition-opacity duration-300"
|
||||
:class="collapsed&&showLightShadow?'opacity-100':'opacity-0'"
|
||||
/>
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 h-14 bg-gradient-to-t from-old-neutral-800 to-transparent pointer-events-none transition-opacity duration-300"
|
||||
:class="collapsed&&showDarkShadow?'opacity-100':'opacity-0'"
|
||||
/>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="flex mt-2">
|
||||
<div title="创建时间" class="flex items-center">
|
||||
<Icon name="lucide:file-clock"/>
|
||||
<div class="ml-1">{{ dateFormat(props.rambling.created_at) }}</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.rambling.updated_at)" class="flex items-center ml-2">
|
||||
<Icon name="lucide:clock-alert" title="上次更新时间"/>
|
||||
<HoverContent>
|
||||
<template #content>
|
||||
<div class="ml-1">
|
||||
{{ dateFormat(props.rambling.updated_at[props.rambling.updated_at.length - 1]) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #hoverContent>
|
||||
<div class="p-1 pr-2">
|
||||
<div v-for="(date,index) of props.rambling.updated_at" :key="index">
|
||||
<div class="block whitespace-nowrap">
|
||||
{{ '第' + index + '次更新' + dateFormat(date) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoverContent>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,57 +1,46 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { toArticleMetaDataType } from '~/types/ArticleMetaData';
|
||||
import { toMetaDataType } from '~/types/PostMetaData';
|
||||
import RamblingCard from '~/pages/index/components/RamblingCard.vue';
|
||||
import ArticleCard from '~/pages/index/components/ArticleCard.vue';
|
||||
|
||||
const { data: articles } = useAsyncData(async () => await queryCollection('content').all());
|
||||
const { data: posts } = useAsyncData(async () => await queryCollection('content').order('published_at', 'DESC').all());
|
||||
|
||||
onMounted(() => {
|
||||
console.log(articles.value);
|
||||
});
|
||||
|
||||
// // 监听断点变化并设置延迟策略
|
||||
// watch(showLeftDetailInfo, (newValue, oldValue) => {
|
||||
// if (newValue && !oldValue) {
|
||||
// // xl -> 2xl (显示) - 需要延迟
|
||||
// shouldDelay.value = true;
|
||||
// isTransitioning.value = true;
|
||||
// } else if (!newValue && oldValue) {
|
||||
// // 2xl -> xl (隐藏) - 不延迟
|
||||
// shouldDelay.value = false;
|
||||
// isTransitioning.value = true;
|
||||
// }
|
||||
//
|
||||
// // 动画完成后重置状态
|
||||
// onMounted(() => {
|
||||
// setTimeout(() => {
|
||||
// isTransitioning.value = false;
|
||||
// }, 400); // 根据你的动画时长调整
|
||||
// console.log(articles.value);
|
||||
// }, 2000);
|
||||
// });
|
||||
type PostItem = NonNullable<typeof posts.value>[number];
|
||||
|
||||
function toArticlePage(article: PostItem) {
|
||||
navigateTo(`/article/${encodeURIComponent(article.id)}`);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mt-4">
|
||||
<div class="table w-full">
|
||||
<div class="sticky top-16 float-left bg-old-neutral-500 h-[100vh]">
|
||||
<div class="relative duration-500 transition-all xl:w-80 w-0 mr-2/3 h-full overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-80 text-white z-50">
|
||||
<div class="">
|
||||
<div class="table w-full mt-6">
|
||||
<div class="sticky top-16 float-left bg-old-neutral-200 dark:bg-old-neutral-800 max-h-[calc(100vh-4rem)]">
|
||||
<div class="relative duration-500 transition-all xl:w-80 w-0 mr-2/3 overflow-hidden">
|
||||
<div class="w-80 top-0 left-0 text-gray-800 dark:text-white p-5">
|
||||
test123456
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="transition-all duration-500 float-right xl:w-[calc(100%-20rem-40px)] w-full">
|
||||
<ArticleCard class="mb-6 w-full transition-shadow duration-300 shadow-lg hover:shadow-old-neutral-600"/>
|
||||
<div v-for="article in posts" :key="article.id" class="mb-6 w-full transition-shadow duration-300 shadow-lg hover:shadow-old-neutral-600 hover:cursor-pointer">
|
||||
<ArticleCard
|
||||
v-for="article in articles" :key="article.id"
|
||||
class="mb-4 w-full"
|
||||
:article="toArticleMetaDataType(article)"/>
|
||||
<ArticleCard/>
|
||||
v-if="!article.draft && article.type === 'article'"
|
||||
:article="toMetaDataType(article)"
|
||||
@click="toArticlePage(article)"/>
|
||||
<RamblingCard
|
||||
v-else-if="!article.draft && article.type === 'rambling'"
|
||||
:rambling="toMetaDataType(article)"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
test
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -5,15 +5,17 @@ export enum DataAnomaly {
|
|||
Invalid = 'DataInvalid',
|
||||
}
|
||||
|
||||
export type ArticleMetaData = {
|
||||
export type PostMetaData = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
created_at: Date | DataAnomaly;
|
||||
category: string;
|
||||
published_at: Date | DataAnomaly;
|
||||
draft: boolean;
|
||||
updated_at: Date[] | DataAnomaly;
|
||||
tags: string[];
|
||||
type: string;
|
||||
tech_stack: string[] | DataAnomaly;
|
||||
tech_stack_percent: number[] | DataAnomaly;
|
||||
tech_stack_icon_names: string[] | DataAnomaly;
|
||||
|
@ -37,8 +39,9 @@ function getDate(data: Record<string, unknown>, key: string): Date | DataAnomaly
|
|||
return date;
|
||||
}
|
||||
|
||||
export function toArticleMetaDataType(src: unknown): ArticleMetaData {
|
||||
export function toMetaDataType(src: unknown): PostMetaData {
|
||||
if (typeof src !== 'object' || src === null) {
|
||||
console.log(src);
|
||||
throw new TypeError('Expected an object');
|
||||
}
|
||||
const data = src as Record<string, unknown>;
|
||||
|
@ -104,6 +107,8 @@ export function toArticleMetaDataType(src: unknown): ArticleMetaData {
|
|||
id: String(data.id),
|
||||
title: String(data.title),
|
||||
description: String(data.description ?? ''),
|
||||
category: String(data.category ?? '无'),
|
||||
type: String(data.type ?? 'article'),
|
||||
created_at: created_at,
|
||||
published_at: published_at,
|
||||
draft: Boolean(data.draft ?? false),
|
||||
|
@ -116,3 +121,26 @@ export function toArticleMetaDataType(src: unknown): ArticleMetaData {
|
|||
word_count: wordCountResult,
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultMetaData = toMetaDataType({
|
||||
id: 'default ID',
|
||||
title: 'ApiFox / Postman 使用WebSocket连接SignalR进行测试需要注意的小问题',
|
||||
description: 'default Description',
|
||||
created_at: new Date('2025-01-01T00:00:00Z'), // 默认创建时间
|
||||
published_at: new Date('2025-01-01T00:01:00Z'), // 默认发布时间
|
||||
draft: true,
|
||||
updated_at: [new Date('2025-01-01T00:02:00Z'), new Date('2025-01-01T00:03:00Z')], // 默认更新时间
|
||||
tags: ['C#', 'TS', 'Windows Professional version with Webstorm 2025', 'Windows Professional version with Visual Studio 2022'],
|
||||
tech_stack: new Map([
|
||||
['Vue', 5],
|
||||
['Nuxt', 3],
|
||||
['TypeScript', 4],
|
||||
['JavaScript', 2],
|
||||
['CSS', 1],
|
||||
['HTML', 1],
|
||||
['Node.js', 2],
|
||||
['Python', 3],
|
||||
['Java', 2],
|
||||
['C#', 1],
|
||||
]),
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import { useBreakpoints } from '@vueuse/core';
|
||||
import breakpoints from '~/configs/breakpoints';
|
||||
|
||||
const breakpointsHelper = useBreakpoints(breakpoints);
|
||||
export default breakpointsHelper;
|
Loading…
Reference in New Issue