Compare commits

...
11 Commits
Author SHA1 Message Date
cnb.bApmeMoSgJA dacc3d5146 cnb 自定义开发容器 2025-12-02 15:02:29 +08:00
cnb.bApmeMoSgJA 2ab2bba546 🔧 添加vscode开发支持 2025-11-25 17:29:47 +08:00
li-chx ef224f1326 🐛 部分依赖优化 Cloudflare构建优化 2025-11-17 11:50:44 +08:00
li-chx 886fcfde70 🐛 更新主界面图片显示CSS 2025-11-16 20:18:48 +08:00
li-chx 0b83b687d9 🐛 6 四个URL能漏改一个 2025-11-05 23:32:31 +08:00
li-chx 9043cf53fd 扬掉旧的掉帧动画系统 严肃使用新动画系统 2025-11-05 23:23:07 +08:00
li-chx 6c963257cf 完善筛选功能 新增一言 完成关于页面 🐛 解决归档页的ssg问题 2025-10-06 03:44:54 +08:00
li-chx 1cefa4c661 🐛 修复潜在的ssg问题 2025-10-05 03:01:52 +08:00
li-chx 51a39d498b 升级到Nuxt4
 完成分类 标签的HTML搭建
🐛 修复ssg相关配置 之前错误的使用了SPA
🐛 修复部分ssg兼容问题
2025-09-26 18:09:05 +08:00
li-chx 75ba2bf5c8 🐛 放弃TS7.0 它还不成熟 2025-09-13 17:26:06 +08:00
li-chx c1be9ebdbb 更新部分CSS 添加TS7.0依赖 有待配置 2025-08-31 02:30:39 +08:00
40 changed files with 874 additions and 360 deletions
+2
View File
@@ -28,3 +28,5 @@ pnpm-workspace.yaml
/client_ chrome.run.xml
/nuxt.run.xml
/server_ nuxt.run.xml
/output.tar.gz
.pnpm-store
+33
View File
@@ -0,0 +1,33 @@
FROM cnbcool/default-build-env:latest
RUN apt-get update && \
apt-get install -y wget unzip lsof nload htop net-tools dnsutils openssh-server zsh openssh-server command-not-found && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
WORKDIR /root
RUN git clone https://git.lichx.top/li_chx/shell-config.git && \
cp -r shell-config/.* . && \
wget https://download.jetbrains.com/webstorm/WebStorm-2025.2.5.tar.gz && \
mkdir -p /ide_cnb && \
tar -zxvf WebStorm-2025.2.5.tar.gz -C /ide_cnb && \
rm WebStorm-2025.2.5.tar.gz && \
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash && \
source ~/.bashrc && \
nvm install 24 && \
npm install --global corepack@latest && \
corepack enable pnpm && \
apt update
# 单独处理 zsh 配置
RUN zsh -c "source ~/.zshrc" && \
echo "zsh" >> ~/.bashrc
RUN curl -fsSL https://code-server.dev/install.sh | sh \
&& code-server --install-extension nuxtr.nuxt-vscode-extentions \
&& code-server --install-extension bradlc.vscode-tailwindcss \
&& code-server --install-extension tencent-cloud.coding-copilot \
&& code-server --install-extension ms-vscode.vs-keybindings
ENV LANG C.UTF-8
ENV LANGUAGE C.UTF-8
+11
View File
@@ -0,0 +1,11 @@
{
"recommendations": [
"vue.volar",
"vue.vscode-typescript-vue-plugin",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-next",
"nuxtr.nuxt-vscode-extentions"
]
}
+26
View File
@@ -0,0 +1,26 @@
{
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.suggest.autoImports": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"vue.codeActions.enabled": true,
"vue.complete.casing.tags": "kebab",
"vue.complete.casing.props": "camel",
"vetur.validation.template": false,
"vetur.validation.script": false,
"vetur.validation.style": false,
"emmet.includeLanguages": {
"vue": "html"
},
"files.associations": {
"*.vue": "vue"
},
"editor.quickSuggestions": {
"strings": true
},
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
+1 -1
View File
@@ -8,4 +8,4 @@ html {
--light-text-color: #1e2939; /*gray-800*/
--light-text-secondary-color: #4a5565; /*gray-600*/
--dark-text-color: #e5e7eb; /*gray-200*/
}
}
+216
View File
@@ -0,0 +1,216 @@
<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 typeGroup = computed(() => {
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);
return [
{ name: '文章', count: articleCount, type: 'article' },
{ name: '絮语', count: ramblingCount, type: 'rambling' },
{ name: '公告', count: announcementCount, type: 'announcement' },
];
});
const typeEnableStatus: Ref<boolean[]> = ref(Array(typeGroup.value.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;
});
const categoriesEnableStatus: Ref<boolean[]> = ref(new Array(categories.value.length).fill(true));
const tagsEnableStatus: Ref<boolean[]> = ref(new Array(tags.value.length).fill(true));
function updateTypeEnableStatus(index: number) {
if (typeEnableStatus.value.reduce((last, cur) => last && cur, true)) {
for (let i = 0; i < typeEnableStatus.value.length; i++) {
if (i !== index) {
typeEnableStatus.value[i] = false;
}
}
} else if (!typeEnableStatus.value.reduce((last, cur, localIndex) => last || (localIndex === index ? false : cur), false)) {
for (let i = 0; i < typeEnableStatus.value.length; i++) {
typeEnableStatus.value[i] = true;
}
} else
typeEnableStatus.value[index] = !typeEnableStatus.value[index];
updateRule();
}
function updateCategoryEnableStatus(index: number) {
if (categoriesEnableStatus.value.reduce((last, cur) => last && cur, true)) {
for (let i = 0; i < categoriesEnableStatus.value.length; i++) {
if (i !== index) {
categoriesEnableStatus.value[i] = false;
}
}
} else if (categoriesEnableStatus.value[index] && !categoriesEnableStatus.value.reduce((last, cur, localIndex) => last || (localIndex === index ? false : cur), false)) {
for (let i = 0; i < categoriesEnableStatus.value.length; i++) {
categoriesEnableStatus.value[i] = true;
}
} else
categoriesEnableStatus.value[index] = !categoriesEnableStatus.value[index];
updateRule();
}
function updateTagEnableStatus(index: number) {
if (tagsEnableStatus.value.reduce((last, cur) => last && cur, true)) {
for (let i = 0; i < tagsEnableStatus.value.length; i++) {
if (i !== index) {
tagsEnableStatus.value[i] = false;
}
}
} else if (tagsEnableStatus.value[index] && !tagsEnableStatus.value.reduce((last, cur, localIndex) => last || (localIndex === index ? false : cur), false)) {
for (let i = 0; i < tagsEnableStatus.value.length; i++) {
tagsEnableStatus.value[i] = true;
}
} else
tagsEnableStatus.value[index] = !tagsEnableStatus.value[index];
updateRule();
}
function updateRule() {
const enabledCategories = categoriesEnableStatus.value.reduce((last, cur, localIndex) => {
if (cur) last.add(categories.value[localIndex]![0]);
return last;
}, new Set<string>());
const enabledTags = tagsEnableStatus.value.reduce((last, cur, localIndex) => {
if (cur) last.add(tags.value[localIndex]![0]);
return last;
}, new Set<string>());
// const enable
emits('filterRuleChange', (post) => {
// type check
let tempAns = false;
for (let i = 0; i < typeEnableStatus.value.length; i++) {
if (typeEnableStatus.value[i] && post.type === typeGroup.value[i]!.type) {
tempAns = true;
}
}
// category check
if (tempAns)
tempAns = false;
else
return false;
if (post.category && enabledCategories.has(post.category))
tempAns = true;
else tempAns = !post.category && enabledCategories.size === categories.value.length;
// tag check
if (tempAns)
tempAns = false;
else
return false;
if (tagsEnableStatus.value.length === enabledTags.size)
tempAns = true;
else for (const tag of post.tags || []) {
if (enabledTags.has(tag)) {
tempAns = true;
break;
}
}
return tempAns;
});
}
</script>
<template>
<div>
<div class="bg-old-neutral-200 dark:bg-old-neutral-800 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 typeGroup"
: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': !typeEnableStatus[index]}"
@click="updateTypeEnableStatus(index)"
>
<div>{{ data.name }}</div>
<div>{{ data.count }}</div>
</div>
</div>
</div>
<div class="bg-old-neutral-200 dark:bg-old-neutral-800 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"
:class="{'text-old-neutral-400': !categoriesEnableStatus[index]}"
@click="updateCategoryEnableStatus(index)"
>
<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 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-shadow duration-300"
:class="{'text-old-neutral-400': !tagsEnableStatus[index]}"
@click="updateTagEnableStatus(index)"
>
<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
View File
@@ -3,7 +3,6 @@
<template>
<div>
</div>
</template>
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
</script>
<template>
<div>
<div class="bg-old-neutral-200 dark:bg-old-neutral-800 p-5">
Author: Lichx
<div>
Contact me:
<a href="mailto:li_chx@qq.com" />
</div>
</div>
</div>
</template>
+35 -5
View File
@@ -9,22 +9,33 @@ const props = withDefaults(defineProps<{
}>(), {
markdown: () => '## Hello World!',
});
console.log(props.markdown);
const eraseHeaderMarkdown = computed(() => props.markdown.replace(/^---[\s\S]*?---\n?/, ''));
const { colorMode } = storeToRefs(useColorModeStore());
const mounted = ref(false);
onMounted(() => {
mounted.value = true;
});
</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">
<MdPreview
v-if="mounted"
:key="editorId + '-' + colorMode"
:editor-id="editorId"
:theme="colorMode"
:model-value="eraseHeaderMarkdown"
class="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 +43,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 +69,5 @@ const { colorMode } = storeToRefs(useColorModeStore());
:global(.dark .md-editor-preview) {
--md-color: var(--ui-text);
}
</style>
+12 -6
View File
@@ -54,7 +54,7 @@ const renderChart = () => {
const techStackPercent = props.techStackPercent as number[];
if (!chartRef.value) return;
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 barHeight = 20;
const gap = 10;
@@ -64,6 +64,7 @@ const renderChart = () => {
chart: {
type: 'bar',
backgroundColor: 'transparent',
reflow: false,
},
credits: {
enabled: false,
@@ -83,8 +84,8 @@ const renderChart = () => {
labels: {
useHTML: true,
formatter: function () {
return `<div style="width: 25px; height: 25px;" title="${fullArr[this.pos][0]}">
${colorMode.value === 'light' ? fullArr[this.pos][2] : fullArr[this.pos][3]}
return `<div style="width: 25px; height: 25px;" title="${fullArr[this.pos]![0]}">
${colorMode.value === 'light' ? fullArr[this.pos]![2] : fullArr[this.pos]![3]}
</div>`;
},
},
@@ -114,7 +115,7 @@ const renderChart = () => {
],
tooltip: {
formatter: function () {
return `${fullArr[this.x][0]} ${toPercent(this.y)}`;
return `${fullArr[this.x]![0]} ${toPercent(this.y)}`;
},
},
plotOptions: {
@@ -127,7 +128,8 @@ const renderChart = () => {
dataLabels: {
enabled: true,
style: {
color: '#fff',
color: colorMode.value === 'light' ? '#4e4d55' : '#fff',
textOutline: 'none',
},
formatter: function () {
return toPercent(this.y); // 自定义条形图值的显示格式
@@ -178,11 +180,15 @@ const scrollbarOptions = {
},
};
const mounted = ref(false);
onMounted(() => {
mounted.value = true;
});
</script>
<template>
<div class="h-full">
<div v-if="noDataAvailable" class="flex items-center justify-center h-full p-8">
<div v-if="!mounted||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
-1
View File
@@ -4,6 +4,5 @@ const breakpoints = {
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
'hidden-logo': '1736px',
};
export default breakpoints;
+1 -1
Submodule content updated: a7cd0a0360...2865b12ed9
+2 -1
View File
@@ -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([]),
+25 -33
View File
@@ -4,6 +4,7 @@ import useColorModeStore from '~/stores/colorModeStore';
import { useWindowScroll } from '@vueuse/core';
const { colorMode } = storeToRefs(useColorModeStore());
const isHome = computed(() => useRoute().path === '/');
const items = ref<NavigationMenuItem[]>([
{
@@ -41,7 +42,6 @@ onMounted(() => {
});
const scrollY = useWindowScroll().y;
const isScrollDown = ref(false);
// gsap.registerPlugin(ScrollTrigger);
watch(scrollY, (newY) => {
if (newY > 0 && !collapsed.value) {
@@ -59,57 +59,47 @@ useRouter().beforeEach(() => {
useRouter().afterEach(() => {
isLoading.value = false;
});
const mounted = ref(false);
onMounted(() => {
mounted.value = true;
});
</script>
<template>
<div class="w-full min-h-[100vh] h-full">
<div class="bg-old-neutral-50 dark:bg-[#0b0d0d] w-full min-h-[100vh] h-full">
<UApp>
<div
:class=" (collapsed ? 'h-[20vh]': 'h-[40vh]')"
class="flex flex-col relative transition-all duration-500 ease-in-out" @mouseenter="() => {
class="flex flex-col relative transition-[height] duration-500 max-h-80">
<!-- header -->
<div
v-if="mounted"
@mouseenter="() => {
if(scrollY === 0) {
collapsed = false;
}
}"
@mouseleave="collapsed = true">
<!-- header -->
<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"
>
}" @mouseleave="collapsed = true">
<div
v-if="colorMode === 'light'"
class="flex h-full w-full absolute bg-[url('/79d52228c770808810a310115567e6790380823a.png')] bg-cover bg-top ">
class="flex bg-top absolute w-full h-full bg-[url('/79d52228c770808810a310115567e6790380823a.webp')] ">
<slot name="header"/>
</div>
<div
v-else
class="flex h-full w-full absolute bg-[url('/anime-8788959.jpg')] bg-cover bg-center">
class="flex bg-top absolute w-full h-full bg-[url('/anime-8788959.webp')]">
<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"
>
<!-- header picture -->
<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"/>
class="opacity-80 max-h-[48px] flex w-full h-full fixed bg-[url('/79d52228c770808810a310115567e6790380823a.webp')] 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"/>
class="opacity-20 max-h-[48px] flex w-full h-full fixed bg-[url('/anime-8788959.webp')] bg-cover bg-center"/>
</div>
</Transition>
</div>
<!-- navbar -->
<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">
@@ -118,8 +108,9 @@ useRouter().afterEach(() => {
<slot name="navbarLeft" :is-scroll-down="isScrollDown"/>
</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">
<UNavigationMenu :items="items" :class="colorMode" class="w-full"/>
class="transition-[width] duration-500 flex 2xl:w-[1240px] xl:w-[1020px] lg:w-[964px] md:w-[708px] sm:w-[580px] w-10/12">
<UNavigationMenu v-if="mounted" :items="items" :class="colorMode" class="w-full"/>
<div v-else class="w-full h-12 animate-pulse"></div>
</div>
<div class="flex-1 overflow-hidden">
<slot name="navbarRight" :is-scroll-down="isScrollDown"/>
@@ -137,10 +128,11 @@ useRouter().afterEach(() => {
</div>
</div>
<div class="flex justify-center items-center duration-500 bg-white dark:bg-[#16191b] h-full">
<!-- content -->
<div class="flex justify-center items-center 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-11/12">
class="transition-[width] 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>
+9 -5
View File
@@ -2,7 +2,6 @@
import tailwindcss from '@tailwindcss/vite';
export default defineNuxtConfig({
ssr: false,
compatibilityDate: '2025-05-15',
devtools: { enabled: false },
vite: {
@@ -19,17 +18,22 @@ export default defineNuxtConfig({
css: ['~/assets/css/main.css'],
ui: {
colorMode: false,
fonts: false,
},
app: {
head: {
title: '随机存取',
htmlAttrs: {
lang: 'zh-CN',
},
meta: [
{ name: 'description', content: 'Lichx 个人博客' },
],
script: [{ src: '/darkVerify.js' }],
},
},
sourcemap: {
server: true,
client: true,
},
// sourcemap: {
// server: true,
// client: true,
// },
});
+35 -19
View File
@@ -10,38 +10,54 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/content": "^3.6.3",
"@nuxt/eslint": "1.5.2",
"@nuxt/icon": "^1.15.0",
"@nuxt/ui": "3.2.0",
"@pinia/nuxt": "^0.11.1",
"@nuxt/content": "^3.7.1",
"@nuxt/eslint": "1.9.0",
"@nuxt/icon": "^2.0.0",
"@nuxt/ui": "4.0.0",
"@pinia/nuxt": "^0.11.2",
"@tailwindcss/vite": "^4.1.11",
"@vue/eslint-config-prettier": "^10.2.0",
"@vueuse/core": "^13.6.0",
"better-sqlite3": "^12.2.0",
"eslint": "^9.0.0",
"@vueuse/core": "^13.9.0",
"better-sqlite3": "^12.4.1",
"eslint": "^9.36.0",
"gsap": "^3.13.0",
"highcharts": "^12.3.0",
"md-editor-v3": "^5.8.4",
"nuxt": "^3.17.6",
"highcharts": "^12.4.0",
"md-editor-v3": "^6.0.1",
"nuxt": "^4.1.2",
"overlayscrollbars-vue": "^0.5.9",
"pinia": "^3.0.3",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.1.11",
"typescript": "^5.6.3",
"vue": "^3.5.17",
"vue": "^3.5.21",
"vue-router": "^4.5.1",
"word-count": "^0.3.1"
},
"packageManager": "pnpm@10.14.0",
"packageManager": "pnpm@10.22.0",
"devDependencies": {
"@stylistic/eslint-plugin": "^5.1.0",
"@iconify-json/clarity": "^1.2.4",
"@iconify-json/codicon": "^1.2.32",
"@iconify-json/fluent": "^1.2.34",
"@iconify-json/lucide": "^1.2.68",
"@iconify-json/material-symbols": "^1.2.40",
"@iconify-json/octicon": "^1.2.19",
"@stylistic/eslint-plugin": "^5.4.0",
"@stylistic/eslint-plugin-jsx": "^4.4.1",
"@vue/eslint-config-typescript": "^14.6.0",
"eslint-plugin-vue": "^10.3.0",
"globals": "^16.3.0",
"less": "^4.4.0",
"overlayscrollbars": "^2.11.5",
"typescript-eslint": "^8.35.1",
"eslint-plugin-vue": "^10.5.0",
"globals": "^16.4.0",
"less": "^4.4.1",
"overlayscrollbars": "^2.12.0",
"typescript-eslint": "^8.44.1",
"vue-eslint-parser": "^10.2.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"better-sqlite3",
"esbuild",
"unrs-resolver",
"vue-demi"
]
}
}
+4 -3
View File
@@ -3,10 +3,11 @@
</script>
<template>
admin
<div>
admin
</div>
</template>
<style scoped>
</style>
</style>
+14 -28
View File
@@ -1,7 +1,13 @@
<script setup lang="ts">
import useColorModeStore from '~/stores/colorModeStore';
import breakpointsHelper from '~/utils/BreakpointsHelper';
import ThemeChange from '~/pages/index/components/ThemeChange.vue';
const hitokoto = ref('加载中...');
onMounted(async () => {
const src = await $fetch<{ hitokoto: string; from_who?: string; from?: string }>('https://v1.hitokoto.cn?c=k');
hitokoto.value = `${src.hitokoto} —— ${src.from_who ? src.from_who : '佚名'}${src.from ? `${src.from}` : '未知来源'}`;
});
</script>
<template>
@@ -25,31 +31,8 @@ import breakpointsHelper from '~/utils/BreakpointsHelper';
<template #navbarRight>
<div class="flex items-center h-full">
<div class="flex-1"/>
<div class="flex-1 flex items-center justify-end duration500 ease-in-out">
<Transition
mode="out-in"
enter-active-class="transition-opacity duration-300 ease-in-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-300 ease-in-out"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<Icon
v-if="useColorModeStore().colorMode === 'dark'"
key="dark"
name="material-symbols:dark-mode"
class="text-2xl cursor-pointer mr-5"
@click="() => useColorModeStore().toggleColorMode()"
/>
<Icon
v-else
key="light"
name="material-symbols:clear-day-rounded"
class="text-2xl cursor-pointer mr-5"
@click="() => useColorModeStore().toggleColorMode()"
/>
</Transition>
<div class="flex-1 flex items-center justify-end ease-in-out">
<ThemeChange />
</div>
</div>
</template>
@@ -61,13 +44,16 @@ 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>
<div class="w-full flex flex-col justify-center items-center p-10 text-old-neutral-500">
<div>
{{ hitokoto }}
</div>
<div>
© 2025 随机存取. 由Lichx制作
</div>
@@ -0,0 +1,45 @@
<script setup lang="tsx">
const techStack = ['Vue', 'Nuxt', 'TypeScript', 'Python', 'Java', 'C#', 'Rust'];
const techStackPercent = [86, 80, 92, 75, 60, 90, 50];
const techStackIconNames = ['mdi:vuejs', 'lineicons:nuxt', 'mdi:language-typescript', 'mdi:language-python', 'mdi:language-java', 'mdi:language-csharp', 'mdi:language-rust'];
const techStackThemeColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#FFB347', '#98D8C8', '#F7DC6F', '#BB8FCE'];
const mounted = ref(false);
onMounted(() => {
mounted.value = true;
});
</script>
<template>
<div>
<div class="light:bg-old-neutral-200 dark:bg-old-neutral-800 p-5 mt-3">
<div class="text-2xl pb-2">关于作者</div>
lichx目前就读于武汉理工大学技术涉猎广泛但不精仍在持续学习中 <br/>
</div>
<div class="light:bg-old-neutral-200 dark:bg-old-neutral-800 p-5 mt-3">
<div class="text-2xl pb-2">技术栈相对熟练度</div>
<TechStackCard
v-if="mounted" async-key="about page" :tech-stack="techStack"
:tech-stack-percent="techStackPercent" :tech-stack-icon-names="techStackIconNames"
:tech-stack-theme-colors="techStackThemeColors"/>
</div>
<div class="light:bg-old-neutral-200 dark:bg-old-neutral-800 p-5 mt-3">
<div class="text-2xl pb-2">在其他渠道关注 / 联系我</div>
<a href="https://github.com/li-chx" title="github,不常用">
<icon name="mdi:github" class="inline-block w-10 h-10 mr-4"/>
</a>
<a href="https://git.lichx.top/li_chx" title="gitea,主要代码托管平台">
<icon name="pajamas:gitea" class="inline-block w-10 h-10 mr-4"/>
</a>
<a href="https://leetcode.cn/u/gallant-paynekan/" title="leetcode,刷点水题骗骗自己">
<icon name="tabler:brand-leetcode" class="inline-block w-10 h-10 mr-4"/>
</a>
<a href="mailto:751176501@qq.com" title="能不能看见全看运气">
<icon name="material-symbols:mail-rounded" class="inline-block w-10 h-10"/>
</a>
</div>
</div>
</template>
<style scoped>
</style>
@@ -0,0 +1,8 @@
<template>
<div class="light:bg-old-neutral-200 dark:bg-old-neutral-800 p-5">
<div class="text-2xl pb-2">关于本站</div>
本站随机存取起这个名字纯粹是因为作者想不出来了最后基于我对该网站的需求决定叫随机存取<br/>
这个网站最重要的还是记录我认为有趣的 学习 / 调试 过程便于我随时取用所以写的时候完全就是梦到哪写到哪<br/>
但是如果本人闲暇之余写的文章能帮到你当是莫大的荣幸<br/>
</div>
</template>
+18
View File
@@ -0,0 +1,18 @@
<script setup lang="ts">
import AuthorDescription from '~/pages/index/about/compoents/AuthorDescription.vue';
import SiteDescription from '~/pages/index/about/compoents/SiteDescription.vue';
</script>
<template>
<div>
<div class="mt-6 mb-6 w-full">
<SiteDescription />
<AuthorDescription />
</div>
</div>
</template>
<style scoped>
</style>
+19 -33
View File
@@ -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,30 +57,21 @@ 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"
>
<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
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>
<div class="flex items-center" @click="toArticlePage(article)">
<div :title="dateFormatToTime(article.published_at)" class="text-sm w-12">
<div :title="dateFormatToTime(article.published_at)" class="text-sm min-w-12">
{{ dateFormatToDate(article.published_at) }}
</div>
<div :title="dateFormatToTime(article.published_at)" class="text-md pl-5">
<div :title="dateFormatToTime(article.published_at)" class="text-md ml-10">
{{ article.title }}
</div>
</div>
+28 -14
View File
@@ -1,34 +1,48 @@
<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 srcPostsMetaData = ref<PostMetaData[]>([]);
const postsMetaData = ref<PostMetaData[]>([]);
const currentChoice = ref('时间');
async function loadPostsMetaData() {
srcPostsMetaData.value = sortMetaData((await queryCollection('content').all()).map((x) => toMetaDataType(x)), 'published_at', true) || [];
srcPostsMetaData.value = srcPostsMetaData.value.filter((x) => !x.draft);
postsMetaData.value = srcPostsMetaData.value;
}
// onMounted(() => {
// setTimeout(() => {
// console.log(articles.value);
// }, 2000);
// });
await loadPostsMetaData();
watch(srcPostsMetaData, () => {
postsMetaData.value = srcPostsMetaData.value || [];
});
const currentChoice = ref<'time' | 'category'>('time');
const choiceItems = ref<RadioGroupItem>([
{ label: '时间', value: 'time' },
{ label: '类别', value: 'category' },
]);
</script>
<template>
<div>
<div class="table w-full mt-6">
<div class="table w-full mt-6 mb-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="relative duration-500 transition-[width] 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">
class="transition-[width] 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) {
@@ -55,7 +55,7 @@ onMounted(() => {
<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">
&nbsp;{{ '第' + 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>
+7 -7
View File
@@ -5,24 +5,24 @@ import ArticleHeader from '~/pages/index/article/[articleID]/components/ArticleH
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>
<div class="table w-full mt-6">
<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)]">
<div class="relative duration-500 transition-all xl:w-80 w-0 mr-2/3 overflow-hidden">
<div class="relative duration-500 transition-[width] 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="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-[width] 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>
+50 -43
View File
@@ -2,16 +2,19 @@
import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
import type { PostMetaData } from '~/types/PostMetaData';
import breakpointsHelper from '~/utils/BreakpointsHelper';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue';
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;
}
@@ -38,87 +44,89 @@ function getCostTime(length: number | DataAnomaly) {
return `${minutes}分钟`;
}
}
const mounted = ref(false);
onMounted(() => {
mounted.value = true;
});
</script>
<template>
<div class="p-5 light:bg-old-neutral-200 dark:bg-old-neutral-800 min-h-64 transition-all duration-500">
<div class="p-5 light:bg-old-neutral-200 dark:bg-old-neutral-800 min-h-64">
<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>
<overlay-scrollbars-component>
{{ metaData?.description }}
</overlay-scrollbars-component>
<div v-if="mounted" class="">
<TechStackCard
: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="lg:w-64 w-0 transition-[width] duration-500"
/>
</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 v-else class="min-w-64"/>
</div>
<hr/>
<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">
&nbsp;{{ '第' + index + '次更新' + dateFormat(date) }}
</div>
@@ -126,15 +134,14 @@ function getCostTime(length: number | DataAnomaly) {
</div>
</template>
</HoverContent>
</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>
@@ -2,18 +2,24 @@
import { DataAnomaly, defaultMetaData } from '~/types/PostMetaData';
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 | undefined, string>([
['rambling', '絮语'],
['announcement', '公告'],
]);
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;
}
@@ -42,127 +48,100 @@ 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}`;
});
const showLightShadow = ref(false);
const showDarkShadow = ref(false);
const showShadow = ref(true);
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;
showShadow.value = false;
setTimeout(() => {
showLightShadow.value = showLight;
showDarkShadow.value = showDark;
showShadow.value = true;
}, 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"
class="p-5 light:bg-old-neutral-200 dark:bg-old-neutral-800 min-h-64"
@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 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!"
class="transition-all duration-500 w-full"/>
class="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'"
class="absolute bottom-0 left-0 right-0 h-14 bg-gradient-to-t from-old-neutral-200 dark:from-old-neutral-800 to-transparent pointer-events-none transition-opacity duration-300"
:class="collapsed && showShadow?'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 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">
&nbsp;{{ '第' + index + '次更新' + dateFormat(date) }}
</div>
+78
View File
@@ -0,0 +1,78 @@
<script setup lang="ts">
import useColorModeStore from '~/stores/colorModeStore';
const colorModeStore = useColorModeStore();
function revealToggle(e?: MouseEvent) {
if (typeof document === 'undefined' || !e || !('startViewTransition' in document)) {
// SSR 或无事件时直接切换或不支持时直接切换
colorModeStore.toggleColorMode();
return;
}
const transition = document.startViewTransition(() => {
colorModeStore.toggleColorMode();
});
transition.ready.then(() => {
// 获取鼠标的坐标
const { clientX, clientY } = e!;
// 计算最大半径
const radius = Math.hypot(
Math.max(clientX, innerWidth - clientX),
Math.max(clientY, innerHeight - clientY),
);
// 圆形动画扩散开始
document.documentElement.animate(
{
clipPath: [
`circle(0% at ${clientX}px ${clientY}px)`,
`circle(${radius}px at ${clientX}px ${clientY}px)`,
],
},
// 设置时间,已经目标伪元素
{
duration: 500,
pseudoElement: '::view-transition-new(root)',
},
);
});
}
</script>
<template>
<Transition
mode="out-in"
enter-active-class="transition-opacity duration-300 ease-in-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-300 ease-in-out"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<Icon
v-if="colorModeStore.colorMode === 'dark'"
key="dark"
name="material-symbols:dark-mode"
class="text-2xl cursor-pointer mr-5"
@click="revealToggle($event)"
/>
<Icon
v-else
key="light"
name="material-symbols:clear-day-rounded"
class="text-2xl cursor-pointer mr-5"
@click="revealToggle($event)"
/>
</Transition>
</template>
<style>
::view-transition-new(root),
::view-transition-old(root) {
animation: none;
}
</style>
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped>
</style>
+57 -24
View File
@@ -1,44 +1,77 @@
<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());
const srcPostsMetaData = ref<PostMetaData[]>([]);
const postsMetaData = ref<PostMetaData[]>([]);
// onMounted(() => {
// setTimeout(() => {
// console.log(articles.value);
// }, 2000);
// });
type PostItem = NonNullable<typeof posts.value>[number];
async function loadPostsMetaData() {
srcPostsMetaData.value = sortMetaData((await queryCollection('content').all()).map((x) => toMetaDataType(x)), 'published_at', true) || [];
srcPostsMetaData.value = srcPostsMetaData.value.filter((x) => !x.draft);
postsMetaData.value = srcPostsMetaData.value;
}
function toArticlePage(article: PostItem) {
await loadPostsMetaData();
function toArticlePage(article: PostMetaData) {
navigateTo(`/article/${encodeURIComponent(article.id)}`);
}
watch(srcPostsMetaData, () => {
postsMetaData.value = srcPostsMetaData.value || [];
});
function filterRuleChange(rule: (data: PostMetaData) => boolean) {
postsMetaData.value = (srcPostsMetaData.value || []).filter(rule);
}
</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 class="table w-full mt-6 table-fixed">
<div class="sticky top-16 float-left max-h-[calc(100vh-4rem)]">
<div class="relative duration-500 transition-[width] xl:w-80 w-0 overflow-hidden">
<div class="w-80 top-0 left-0 text-gray-800 dark:text-white">
<ArticleDescriptionCards
v-if="postsMetaData"
class="mb-5" :posts-meta-data="srcPostsMetaData!"
@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">
<div class="transition-[width] 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="post in postsMetaData" :key="post.id"
class="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="mb-6 w-full"
:meta-data="post"
@click="toArticlePage(post)"/>
<SimpleCard
v-else-if="!post.draft && (post.type === 'rambling' || post.type === 'announcement')"
class="mb-6 w-full"
:meta-data="post"/>
<div v-else-if="post.draft">
</div>
<div v-else>
{{post}}
</div>
</div>
<div v-if="postsMetaData.length === 0" class="w-full">
<div
class="w-full light:bg-old-neutral-200 dark:bg-old-neutral-800 transition-shadow duration-300 shadow-lg hover:shadow-old-neutral-600 hover:cursor-pointer">
<div class="pt-5 text-center text-2xl">
没有找到符合条件的文章
</div>
<div class="pt-3 pb-3 text-center text-sm text-old-neutral-500">
tips类型分类标签间的关系为且一类筛选下各个选项间的关系为或
</div>
</div>
</div>
</div>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+1 -3
View File
@@ -1,8 +1,6 @@
// darkVerify.js
if (
localStorage.getItem('system-theme-mode') === "dark" ||
(!localStorage.getItem('system-theme-mode') &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
document.querySelector('html').classList.add('dark');
document.querySelector('html').classList.remove('light');
+6 -3
View File
@@ -1,13 +1,16 @@
const getInitialMode = () => {
function getInitialMode(): 'light' | 'dark' {
if (typeof window !== 'undefined') {
// 优先用 html 的 class
if (document.documentElement.classList.contains('dark')) return 'dark';
if (document.documentElement.classList.contains('light')) return 'light';
// 其次用 localStorage
return localStorage.getItem('system-theme-mode') || 'light';
const val = localStorage.getItem('system-theme-mode');
if (val === 'dark') return 'dark';
if (val === 'light') return 'light';
return 'light'; // 默认
}
return 'light'; // SSR 默认
};
}
const useColorModeStore = defineStore('colorMode', {
state: () => ({
-43
View File
@@ -1,43 +0,0 @@
// tailwind config file
import plugin from 'tailwindcss/plugin';
import tailwindScrollbar from 'tailwind-scrollbar';
import breakpoints from '~/configs/breakpoints';
export default {
mode: 'jit',
darkMode: 'class',
theme: {
extend: {
colors: {},
letterSpacing: {
doublewidest: '.2em',
},
},
screens: breakpoints,
},
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
'.scrollbar-hide': {
/* IE and Edge */
'-ms-overflow-style': 'none',
/* Firefox */
'scrollbar-width': 'none',
/* Safari and Chrome */
'&::-webkit-scrollbar': {
display: 'none',
},
},
});
}),
tailwindScrollbar,
],
content: [
'./app.vue',
'./components/**/*.{vue,js,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
],
};
+34
View File
@@ -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);
});
}
}
+14 -8
View File
@@ -4,6 +4,9 @@ export function rgbToHsl(rgb: number[]) {
throw new Error('Input must be an array of three numbers representing RGB values.');
}
let [r, g, b] = rgb;
r = r!;
b = b!;
g = g!;
r /= 255;
g /= 255;
b /= 255;
@@ -36,6 +39,9 @@ export function hslToRgb(hsl: number[]) {
throw new Error('Input must be an array of three numbers representing HSL values.');
}
let [h, s, l] = hsl;
h = h!;
s = s!;
l = l!;
h /= 360;
s /= 100;
l /= 100;
@@ -65,16 +71,16 @@ export function toRGBArray(color: string): number[] {
// 处理 rgb/rgba
const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/);
if (rgbMatch) {
return [parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3])];
return [parseInt(rgbMatch[1]!), parseInt(rgbMatch[2]!), parseInt(rgbMatch[3]!)];
}
// 处理 #fff 或 #ffffff
const hexMatch = color.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
if (hexMatch) {
let hex = hexMatch[1];
if (hex.length === 3) {
hex = hex.split('').map((x) => x + x).join('');
if (hex!.length === 3) {
hex = hex!.split('').map((x) => x + x).join('');
}
const num = parseInt(hex, 16);
const num = parseInt(hex!, 16);
return [
(num >> 16) & 255,
(num >> 8) & 255,
@@ -97,14 +103,14 @@ export function toHexString(rgb: number[]): string {
export function toLightColor(rgb: number[]): number[] {
const hsl = rgbToHsl(rgb);
if (hsl[2] < 50)
hsl[2] = hsl[2] / 5 + 50; // 增加亮度
if (hsl[2]! < 50)
hsl[2] = hsl[2]! / 5 + 50; //
return hslToRgb(hsl);
}
export function toDarkColor(rgb: number[]): number[] {
const hsl = rgbToHsl(rgb);
if (hsl[2] > 50)
hsl[2] = 50 - (hsl[2] - 50) / 5; // 减少亮度
if (hsl[2]! > 50)
hsl[2] = 50 - (hsl[2]! - 50) / 5; // 减少亮度
return hslToRgb(hsl);
}