✨ 更新部分CSS 添加TS7.0依赖 有待配置
This commit is contained in:
parent
363eeb74dd
commit
c1be9ebdbb
|
@ -28,3 +28,4 @@ pnpm-workspace.yaml
|
|||
/client_ chrome.run.xml
|
||||
/nuxt.run.xml
|
||||
/server_ nuxt.run.xml
|
||||
/output.tar.gz
|
||||
|
|
|
@ -8,4 +8,4 @@ html {
|
|||
--light-text-color: #1e2939; /*gray-800*/
|
||||
--light-text-secondary-color: #4a5565; /*gray-600*/
|
||||
--dark-text-color: #e5e7eb; /*gray-200*/
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<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>
|
||||
|
||||
<template>
|
||||
<div class="transition-colors duration-500">
|
||||
<div>
|
||||
<div v-if="showType === ''" class="flex">
|
||||
<div
|
||||
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>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -17,14 +17,13 @@ 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 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"/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:global(.md-editor) {
|
||||
:deep(.md-editor) {
|
||||
--md-bk-color: #e5e5e5;
|
||||
--md-theme-heading-1-color: #fff;
|
||||
transition-property: all;
|
||||
|
@ -32,6 +31,24 @@ const { colorMode } = storeToRefs(useColorModeStore());
|
|||
--tw-ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
}
|
||||
:deep(.md-editor-preview blockquote) {
|
||||
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));
|
||||
}
|
||||
|
||||
:deep(.md-editor-preview .md-editor-code .md-editor-code-head) {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
:deep(ul) {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
:deep(ol) {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
:global(.dark .md-editor) {
|
||||
--md-bk-color: #262626;
|
||||
|
@ -40,4 +57,5 @@ const { colorMode } = storeToRefs(useColorModeStore());
|
|||
:global(.dark .md-editor-preview) {
|
||||
--md-color: var(--ui-text);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
2
content
2
content
|
@ -1 +1 @@
|
|||
Subproject commit a7cd0a03602ed93da797632e5616211f40868125
|
||||
Subproject commit e4c08e4b0aa49ec629020047c88f43664bda0ba2
|
|
@ -10,7 +10,8 @@ const schema = z.object({
|
|||
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'),
|
||||
type: z.enum(['article', 'rambling', 'announcement']).default('article'),
|
||||
isPinned: z.boolean().default(false),
|
||||
tech_stack: z.array(z.string()).default([]),
|
||||
tech_stack_percent: z.array(z.number()).default([]),
|
||||
tech_stack_icon_names: z.array(z.string()).default([]),
|
||||
|
|
|
@ -22,9 +22,13 @@ export default defineNuxtConfig({
|
|||
},
|
||||
app: {
|
||||
head: {
|
||||
title: '随机存取',
|
||||
htmlAttrs: {
|
||||
lang: 'zh-CN',
|
||||
},
|
||||
meta: [
|
||||
{ name: 'description', content: 'Lichx 个人博客' },
|
||||
],
|
||||
script: [{ src: '/darkVerify.js' }],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"@nuxt/ui": "3.2.0",
|
||||
"@pinia/nuxt": "^0.11.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@typescript/native-preview": "7.0.0-dev.20250830.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
|
@ -32,7 +33,7 @@
|
|||
"vue-router": "^4.5.1",
|
||||
"word-count": "^0.3.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.14.0",
|
||||
"packageManager": "pnpm@10.15.0",
|
||||
"devDependencies": {
|
||||
"@stylistic/eslint-plugin": "^5.1.0",
|
||||
"@stylistic/eslint-plugin-jsx": "^4.4.1",
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
admin
|
||||
|
||||
<div>
|
||||
admin
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -61,9 +61,9 @@ import breakpointsHelper from '~/utils/BreakpointsHelper';
|
|||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div>
|
||||
<div class="max-w-full">
|
||||
<NuxtRouteAnnouncer/>
|
||||
<NuxtPage/>
|
||||
<NuxtPage class="max-w-full"/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { DataAnomaly } from '~/types/PostMetaData';
|
||||
import { DataAnomaly, sortMetaData } from '~/types/PostMetaData';
|
||||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
|
||||
const articles = defineModel<PostMetaData[]>('articles', {
|
||||
const articles = defineModel<PostMetaData[]>('metadata', {
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
|
@ -10,28 +10,23 @@ const props = withDefaults(defineProps<{
|
|||
currentChoice?: 'time' | 'category';
|
||||
}>(),
|
||||
{
|
||||
currentChoice: 'time',
|
||||
currentChoice: 'time' as ('time' | 'category'),
|
||||
});
|
||||
|
||||
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());
|
||||
sortMetaData(articles.value, 'published_at');
|
||||
} 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);
|
||||
});
|
||||
sortMetaData(articles.value, 'category');
|
||||
}
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
function toArticlePage(article: PostMetaData) {
|
||||
navigateTo(`/article/${encodeURIComponent(article.id)}`);
|
||||
}
|
||||
|
||||
function getYear(article: PostMetaData) {
|
||||
return new Date(article.published_at).getFullYear();
|
||||
return new Date(article?.published_at || 0).getFullYear();
|
||||
}
|
||||
|
||||
function dateFormatToTime(date: Date | DataAnomaly) {
|
||||
|
@ -62,16 +57,16 @@ function dateFormatToDate(date: Date | DataAnomaly) {
|
|||
<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"
|
||||
>
|
||||
<!-- <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])"
|
||||
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>
|
||||
|
@ -80,7 +75,7 @@ function dateFormatToDate(date: Date | DataAnomaly) {
|
|||
class="year-marker relative text-indigo-300 text-2xl pt-3 pb-3">
|
||||
{{ article.category }}
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- </Transition>-->
|
||||
<div class="flex items-center" @click="toArticlePage(article)">
|
||||
<div :title="dateFormatToTime(article.published_at)" class="text-sm w-12">
|
||||
{{ dateFormatToDate(article.published_at) }}
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { toMetaDataType } from '~/types/PostMetaData';
|
||||
import { sortMetaData, toMetaDataType } from '~/types/PostMetaData';
|
||||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
import TimeLine from '~/pages/index/archive/components/TimeLine.vue';
|
||||
import type { RadioGroupItem } from '@nuxt/ui';
|
||||
|
||||
const { data: articles } = useAsyncData(async () => (await queryCollection('content').order('published_at', 'DESC').all()).map((article) => toMetaDataType(article)));
|
||||
const { data: srcPostsMetaData } = useAsyncData(async () => sortMetaData((await queryCollection('content').all()).map((x) => toMetaDataType(x)), 'published_at', true));
|
||||
const postsMetaData = ref<PostMetaData[]>([]);
|
||||
|
||||
const currentChoice = ref('时间');
|
||||
const currentChoice = ref<'time' | 'category'>('time');
|
||||
|
||||
// onMounted(() => {
|
||||
// setTimeout(() => {
|
||||
// console.log(articles.value);
|
||||
// }, 2000);
|
||||
// });
|
||||
watch(srcPostsMetaData, () => {
|
||||
postsMetaData.value = srcPostsMetaData.value?.filter((x) => !x.draft) || [];
|
||||
});
|
||||
|
||||
const choiceItems = ref<RadioGroupItem>([
|
||||
{ label: '时间', value: 'time' },
|
||||
{ label: '类别', value: 'category' },
|
||||
]);
|
||||
|
||||
</script>
|
||||
<template>
|
||||
|
@ -19,16 +25,16 @@ const currentChoice = ref('时间');
|
|||
<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"
|
||||
v-model="currentChoice" orientation="horizontal" variant="table" :items="choiceItems as any[]" size="sm"
|
||||
class="mb-5"/>
|
||||
<TimeLine v-if="articles" v-model:articles="articles!" :current-choice="currentChoice=== '时间'? 'time' : 'category'"/>
|
||||
<TimeLine v-if="postsMetaData" v-model:metadata="postsMetaData" :current-choice="currentChoice"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,10 +4,10 @@ import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
|
|||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
import breakpointsHelper from '~/utils/BreakpointsHelper';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
article?: PostMetaData;
|
||||
withDefaults(defineProps<{
|
||||
metaData?: PostMetaData;
|
||||
}>(), {
|
||||
article: () => defaultMetaData,
|
||||
metaData: () => defaultMetaData,
|
||||
});
|
||||
|
||||
function dateFormat(date: Date | DataAnomaly) {
|
||||
|
@ -51,11 +51,11 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 pb-0 light:bg-old-neutral-200 dark:bg-old-neutral-800">
|
||||
<div class="p-6 pb-0 light:bg-old-neutral-200 dark:bg-old-neutral-800 transition-colors duration-500">
|
||||
<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 }}
|
||||
{{ metaData.title }}
|
||||
</div>
|
||||
<Icon
|
||||
name="lucide:chevron-down" class="text-2xl transition-transform duration-300 mr-5"
|
||||
|
@ -68,14 +68,14 @@ onMounted(() => {
|
|||
<div title="发布时间" class="flex items-center">
|
||||
<Icon name="lucide:clock-arrow-up"/>
|
||||
<div class="ml-1">
|
||||
{{ dateFormat(props.article.published_at) }}
|
||||
{{ dateFormat(metaData.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 }}
|
||||
{{ metaData.category }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -85,14 +85,14 @@ onMounted(() => {
|
|||
<div title="字数" class="flex items-center">
|
||||
<Icon name="fluent:text-word-count-20-filled"/>
|
||||
<div class="ml-1 inline">
|
||||
{{ props.article.word_count }}字
|
||||
{{ metaData.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) }}
|
||||
{{ getCostTime(metaData.word_count) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -100,19 +100,19 @@ onMounted(() => {
|
|||
<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 class="ml-1">{{ dateFormat(metaData.created_at) }}</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.article.updated_at)" class="flex items-center ml-2">
|
||||
<div v-if="Array.isArray(metaData.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]) }}
|
||||
{{ dateFormat(metaData.updated_at[metaData.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 v-for="(date,index) of metaData.updated_at" :key="index">
|
||||
<div class="block whitespace-nowrap">
|
||||
{{ '第' + index + '次更新' + dateFormat(date) }}
|
||||
</div>
|
||||
|
@ -123,9 +123,9 @@ onMounted(() => {
|
|||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.article.tags)" class="flex items-center">
|
||||
<div v-if="Array.isArray(metaData.tags)" class="flex items-center">
|
||||
<Icon name="clarity:tags-solid"/>
|
||||
<div v-for="(tag,index) of props.article.tags" :key="index">
|
||||
<div v-for="(tag,index) of metaData.tags" :key="index">
|
||||
<div class="ml-1">{{ tag }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -140,11 +140,11 @@ onMounted(() => {
|
|||
>
|
||||
<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"
|
||||
:async-key="'stack:' + metaData.id"
|
||||
:tech-stack="metaData.tech_stack"
|
||||
:tech-stack-icon-names="metaData.tech_stack_icon_names"
|
||||
:tech-stack-theme-colors="metaData.tech_stack_theme_colors"
|
||||
:tech-stack-percent="metaData.tech_stack_percent"
|
||||
class="w-64"
|
||||
/>
|
||||
</Transition>
|
||||
|
|
|
@ -11,18 +11,19 @@ const editorId = 'article-previewer';
|
|||
|
||||
<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="table w-full mt-6 mb-6 table-fixed">
|
||||
<div class="sticky top-16 float-left bg-old-neutral-200 dark:bg-old-neutral-800 max-h-[calc(100vh-4rem)] transition-colors duration-500">
|
||||
<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 class="w-80 top-0 left-0 text-gray-800 dark:text-white p-5 transition-colors duration-500">
|
||||
<div class="text-3xl mb-2">目录</div>
|
||||
<MdCatalog :editor-id="editorId" :scroll-element="'html'" class=""/>
|
||||
</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)"/>
|
||||
<div class="transition-all duration-500 float-right xl:w-[calc(100%-20rem-40px)] w-full max-w-full">
|
||||
<ArticleHeader v-if="article" class="w-full" :meta-data="toMetaDataType(article)"/>
|
||||
<!-- <ArticleCard v-if="article" class=" w-full" :article="toArticleMetaDataType(article)"/>-->
|
||||
<ReadonlyMdEditor :editor-id="editorId" :markdown="article?.rawbody"/>
|
||||
<ReadonlyMdEditor :editor-id="editorId" :markdown="article?.rawbody" class="p-5 max-w-full"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,14 +4,17 @@ import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
|
|||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
import breakpointsHelper from '~/utils/BreakpointsHelper';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
article?: PostMetaData;
|
||||
withDefaults(defineProps<{
|
||||
metaData?: PostMetaData;
|
||||
}>(),
|
||||
{
|
||||
article: () => defaultMetaData,
|
||||
metaData: () => defaultMetaData,
|
||||
});
|
||||
|
||||
function dateFormat(date: Date | DataAnomaly) {
|
||||
function dateFormat(date: Date | DataAnomaly | undefined) {
|
||||
if (!date) {
|
||||
return 'date undefined';
|
||||
}
|
||||
if (date === DataAnomaly.DataNotFound || date === DataAnomaly.Invalid) {
|
||||
return date;
|
||||
}
|
||||
|
@ -24,7 +27,10 @@ function dateFormat(date: Date | DataAnomaly) {
|
|||
});
|
||||
}
|
||||
|
||||
function getCostTime(length: number | DataAnomaly) {
|
||||
function getCostTime(length: number | DataAnomaly | undefined) {
|
||||
if (!length) {
|
||||
return 'length undefined';
|
||||
}
|
||||
if (length === DataAnomaly.DataNotFound || length === DataAnomaly.Invalid) {
|
||||
return length;
|
||||
}
|
||||
|
@ -43,45 +49,50 @@ function getCostTime(length: number | DataAnomaly) {
|
|||
<template>
|
||||
<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 }}
|
||||
{{ metaData?.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.article.published_at) }}
|
||||
{{ dateFormat(metaData?.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 }}
|
||||
{{ metaData?.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.article.word_count }}字
|
||||
{{ metaData?.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.article.word_count) }}
|
||||
{{ getCostTime(metaData?.word_count) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="metaData?.isPinned" class="flex items-center ml-2">
|
||||
<Icon name="codicon:pinned"/>
|
||||
<div class="ml-1 text-nowrap">
|
||||
置顶
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex mt-2 justify-between h-28">
|
||||
<div>
|
||||
<div class="">
|
||||
{{ props.article.description }}
|
||||
<div class="overflow-y-auto">
|
||||
{{ metaData?.description }}
|
||||
</div>
|
||||
</div>
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-500 ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
|
@ -92,12 +103,12 @@ function getCostTime(length: number | DataAnomaly) {
|
|||
>
|
||||
<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"
|
||||
:async-key="'stack:' + metaData?.id"
|
||||
:tech-stack="metaData?.tech_stack"
|
||||
:tech-stack-icon-names="metaData?.tech_stack_icon_names"
|
||||
:tech-stack-theme-colors="metaData?.tech_stack_theme_colors"
|
||||
:tech-stack-percent="metaData?.tech_stack_percent"
|
||||
class="min-w-64"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
|
@ -106,19 +117,19 @@ function getCostTime(length: number | DataAnomaly) {
|
|||
<div class="flex mt-2">
|
||||
<div title="创建时间" class="flex items-center">
|
||||
<Icon name="lucide:file-clock"/>
|
||||
<div class="ml-1">{{ dateFormat(props.article.created_at) }}</div>
|
||||
<div class="ml-1">{{ dateFormat(metaData?.created_at) }}</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.article.updated_at)" class="flex items-center ml-2">
|
||||
<div v-if="Array.isArray(metaData?.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]) }}
|
||||
{{ dateFormat(metaData?.updated_at[metaData?.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 v-for="(date,index) of metaData?.updated_at" :key="index">
|
||||
<div class="block whitespace-nowrap">
|
||||
{{ '第' + index + '次更新' + dateFormat(date) }}
|
||||
</div>
|
||||
|
@ -129,12 +140,12 @@ function getCostTime(length: number | DataAnomaly) {
|
|||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.article.tags)" class="flex items-top">
|
||||
<div v-if="Array.isArray(metaData?.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 v-for="(tag,index) of metaData?.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 v-if="index !== metaData?.tags.length - 1" class="inline">,</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,13 +5,17 @@ import type { PostMetaData } from '~/types/PostMetaData';
|
|||
import useColorModeStore from '~/stores/colorModeStore';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
rambling?: PostMetaData;
|
||||
metaData?: PostMetaData;
|
||||
}>(),
|
||||
{
|
||||
rambling: () => defaultMetaData,
|
||||
metaData: () => defaultMetaData,
|
||||
});
|
||||
const { data: rawbody } = useAsyncData(async () => (await queryCollection('content').where('id', '=', props.rambling.id).first())?.rawbody);
|
||||
const collapsed = ref(false);
|
||||
const { data: rawbody } = useAsyncData('simpleCard:' + props.metaData.id, async () => (await queryCollection('content').where('id', '=', props.metaData.id).first())?.rawbody);
|
||||
const collapsed = ref(true);
|
||||
const typeChinese = new Map<string, string>([
|
||||
['rambling', '絮语'],
|
||||
['announcement', '公告'],
|
||||
]);
|
||||
|
||||
function dateFormat(date: Date | DataAnomaly) {
|
||||
if (date === DataAnomaly.DataNotFound || date === DataAnomaly.Invalid) {
|
||||
|
@ -42,7 +46,7 @@ function getCostTime(length: number | DataAnomaly) {
|
|||
}
|
||||
|
||||
const safeEditorId = computed(() => {
|
||||
const encoded = btoa(encodeURIComponent(props.rambling.id))
|
||||
const encoded = btoa(encodeURIComponent(props.metaData.id))
|
||||
.replace(/[+/=]/g, '_'); // 替换 Base64 中的特殊字符
|
||||
return `rambling_${encoded}`;
|
||||
});
|
||||
|
@ -98,41 +102,48 @@ onUnmounted(() => {
|
|||
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 }}
|
||||
{{ (typeChinese.get(metaData.type) || 'unknown Type') + ':' }}{{ props.metaData.title }}
|
||||
</div>
|
||||
<div class="flex items-center mt-2 max-w-96 overflow-hidden">
|
||||
<div class="flex items-center mt-2 max-w-[400px] 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) }}
|
||||
{{ dateFormat(props.metaData.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 }}
|
||||
{{ props.metaData.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 }}字
|
||||
{{ props.metaData.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) }}
|
||||
{{ getCostTime(props.metaData.word_count) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="metaData.isPinned" class="flex items-center ml-2">
|
||||
<Icon name="codicon:pinned"/>
|
||||
<div class="ml-1 text-nowrap">
|
||||
置顶
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="relative flex mt-2 justify-between overflow-hidden transition-all duration-300 ease-in-out dura"
|
||||
class="relative flex mt-2 justify-between overflow-hidden transition-all duration-300 ease-in-out min-h-[8.5rem]"
|
||||
:class="{'max-h-[8.5rem]' : collapsed, 'max-h-[100vh]':!collapsed}">
|
||||
<ReadonlyMdEditor
|
||||
v-if="rawbody" :editor-id="safeEditorId" :markdown="rawbody!"
|
||||
|
@ -150,19 +161,19 @@ onUnmounted(() => {
|
|||
<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 class="ml-1">{{ dateFormat(props.metaData.created_at) }}</div>
|
||||
</div>
|
||||
<div v-if="Array.isArray(props.rambling.updated_at)" class="flex items-center ml-2">
|
||||
<div v-if="Array.isArray(props.metaData.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]) }}
|
||||
{{ dateFormat(props.metaData.updated_at[props.metaData.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 v-for="(date,index) of props.metaData.updated_at" :key="index">
|
||||
<div class="block whitespace-nowrap">
|
||||
{{ '第' + index + '次更新' + dateFormat(date) }}
|
||||
</div>
|
|
@ -1,44 +1,56 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { toMetaDataType } from '~/types/PostMetaData';
|
||||
import RamblingCard from '~/pages/index/components/RamblingCard.vue';
|
||||
import { sortMetaData, toMetaDataType } from '~/types/PostMetaData';
|
||||
import type { PostMetaData } from '~/types/PostMetaData';
|
||||
import SimpleCard from '~/pages/index/components/SimpleCard.vue';
|
||||
import ArticleCard from '~/pages/index/components/ArticleCard.vue';
|
||||
|
||||
const { data: posts } = useAsyncData(async () => await queryCollection('content').order('published_at', 'DESC').all());
|
||||
|
||||
// onMounted(() => {
|
||||
// setTimeout(() => {
|
||||
// console.log(articles.value);
|
||||
// }, 2000);
|
||||
// });
|
||||
type PostItem = NonNullable<typeof posts.value>[number];
|
||||
|
||||
function toArticlePage(article: PostItem) {
|
||||
const { data: srcPostsMetaData } = useAsyncData(async () => sortMetaData((await queryCollection('content').all()).map((x) => toMetaDataType(x)), 'published_at', true));
|
||||
const postsMetaData = ref<PostMetaData[]>([]);
|
||||
function toArticlePage(article: PostMetaData) {
|
||||
navigateTo(`/article/${encodeURIComponent(article.id)}`);
|
||||
}
|
||||
watch(srcPostsMetaData, () => {
|
||||
postsMetaData.value = srcPostsMetaData.value || [];
|
||||
});
|
||||
|
||||
//
|
||||
// async function loadMetaData() {
|
||||
//
|
||||
// }
|
||||
function filterRuleChange(rule: string) {
|
||||
if (rule === '')
|
||||
postsMetaData.value = srcPostsMetaData.value || [];
|
||||
else
|
||||
postsMetaData.value = (srcPostsMetaData.value || []).filter((post) => post.type === rule);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="table w-full mt-6">
|
||||
<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="relative duration-500 transition-all xl:w-80 w-0 mr-2/3 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">
|
||||
test123456
|
||||
<PersonalCard
|
||||
v-if="postsMetaData" :posts-meta-data="postsMetaData!"
|
||||
@filter-rule-change="filterRuleChange"/>
|
||||
</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 class="mb-6 w-full transition-shadow duration-300 shadow-lg hover:shadow-old-neutral-600"/>-->
|
||||
<div
|
||||
v-for="post in postsMetaData" :key="post.id"
|
||||
class="mb-6 w-full transition-shadow duration-300 shadow-lg hover:shadow-old-neutral-600 hover:cursor-pointer">
|
||||
<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)"/>
|
||||
v-if="!post.draft && post.type === 'article'"
|
||||
class="w-full"
|
||||
:meta-data="post"
|
||||
@click="toArticlePage(post)"/>
|
||||
<SimpleCard
|
||||
v-else-if="!post.draft && (post.type === 'rambling' || post.type === 'announcement')"
|
||||
:meta-data="post"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
|
@ -5,6 +5,10 @@ export enum DataAnomaly {
|
|||
Invalid = 'DataInvalid',
|
||||
}
|
||||
|
||||
export function equalToDataAnomaly(value: unknown): value is DataAnomaly {
|
||||
return value === DataAnomaly.DataNotFound || value === DataAnomaly.Invalid;
|
||||
}
|
||||
|
||||
export type PostMetaData = {
|
||||
id: string;
|
||||
title: string;
|
||||
|
@ -13,6 +17,7 @@ export type PostMetaData = {
|
|||
category: string;
|
||||
published_at: Date | DataAnomaly;
|
||||
draft: boolean;
|
||||
isPinned: boolean;
|
||||
updated_at: Date[] | DataAnomaly;
|
||||
tags: string[];
|
||||
type: string;
|
||||
|
@ -112,6 +117,7 @@ export function toMetaDataType(src: unknown): PostMetaData {
|
|||
created_at: created_at,
|
||||
published_at: published_at,
|
||||
draft: Boolean(data.draft ?? false),
|
||||
isPinned: Boolean(data.isPinned ?? false),
|
||||
updated_at: updated_at,
|
||||
tags: Array.isArray(data.tags) ? data.tags.map(String) : [],
|
||||
tech_stack: tech_stack,
|
||||
|
@ -129,6 +135,7 @@ export const defaultMetaData = toMetaDataType({
|
|||
created_at: new Date('2025-01-01T00:00:00Z'), // 默认创建时间
|
||||
published_at: new Date('2025-01-01T00:01:00Z'), // 默认发布时间
|
||||
draft: true,
|
||||
isPinned: false,
|
||||
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([
|
||||
|
@ -144,3 +151,30 @@ export const defaultMetaData = toMetaDataType({
|
|||
['C#', 1],
|
||||
]),
|
||||
});
|
||||
|
||||
export function sortMetaData(metaData: PostMetaData[], key: keyof PostMetaData, considerPining = false, descending = true) {
|
||||
if (key === 'published_at') {
|
||||
return metaData.sort((a, b) => {
|
||||
let aKey: Date | DataAnomaly = a[key] || new Date(0);
|
||||
let bKey: Date | DataAnomaly = b[key] || new Date(0);
|
||||
if (equalToDataAnomaly(aKey)) aKey = new Date(0);
|
||||
if (equalToDataAnomaly(bKey)) bKey = new Date(0);
|
||||
if (considerPining) {
|
||||
if (a.isPinned && !b.isPinned) return -1;
|
||||
if (!a.isPinned && b.isPinned) return 1;
|
||||
}
|
||||
return descending ? bKey.getTime() - aKey.getTime() : aKey.getTime() - bKey.getTime();
|
||||
});
|
||||
}
|
||||
if (key === 'category') {
|
||||
return metaData.sort((a, b) => {
|
||||
const aKey = String(a[key]);
|
||||
const bKey = String(b[key]);
|
||||
if (considerPining) {
|
||||
if (a.isPinned && !b.isPinned) return -1;
|
||||
if (!a.isPinned && b.isPinned) return 1;
|
||||
}
|
||||
return descending ? bKey.localeCompare(aKey) : aKey.localeCompare(bKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue