Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/nuxthub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on: push

jobs:
deploy:
name: "Deploy to NuxtHub"
name: Deploy to NuxtHub
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -19,7 +19,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache: pnpm

- name: Install dependencies
run: pnpm install
Expand Down
30 changes: 17 additions & 13 deletions app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,9 @@ export default defineAppConfig({
match: ['blog-page', 'blog-tag-tag', 'blog-slug', 'blog-tag-tag-page'],
},
{
label: '项目',
href: '/projects',
match: ['projects'],
},
{
label: '留言墙',
href: '/guestbook',
match: ['guestbook'],
},
{
label: '关于',
href: '/about',
match: ['about'],
label: '想法',
href: '/thoughts',
match: ['thoughts'],
},
] as NavigationItem[],

Expand Down Expand Up @@ -77,5 +67,19 @@ export default defineAppConfig({
},
],
},
{
label: '想法管理',
href: '/admin/thoughts/create',
match: ['admin-thoughts-create'],
icon: 'i-mingcute:lightbulb-line',
children: [
{
label: '新建想法',
href: '/admin/thoughts/create',
match: ['admin-thoughts-create'],
icon: 'i-mingcute:add-line',
},
],
},
] as AdminNavigationItem[],
})
31 changes: 31 additions & 0 deletions app/components/modules/home/thought-item.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import type { SerializeObject } from 'nitropack/types'
import type { Thought } from '~~/shared/types/thought'

interface Props {
thought: SerializeObject<Thought>
}
const { thought } = defineProps<Props>()
</script>

<template>
<article flex="~ col items-start" relative>
<div v-if="thought.mood" text="2xl" mb-2>
{{ thought.mood }}
</div>
<MDCRenderer
class="relative z-10 w-full prose dark:prose-invert"
:body="thought.body"
/>
<div
flex="~ items-center"
pl-3.5 text-sm
text="zinc-500 dark:zinc-500"
z-10 relative mt-2
>
<NuxtTime :datetime="thought.publishedAt" time-zone="Asia/Shanghai">
{{ useDateFormat(() => thought.publishedAt, 'MMM DD YYYY', { locales: 'zh-Hans' }) }}
</NuxtTime>
</div>
</article>
</template>
42 changes: 42 additions & 0 deletions app/components/modules/home/thought-timeline.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
import type { SerializeObject } from 'nitropack/types'
import type { Thought } from '~~/shared/types/thought'

interface Props {
thoughts: SerializeObject<Thought>[]
}

const { thoughts } = defineProps<Props>()

const groupedThoughts = computed(() => {
const groups: Record<string, SerializeObject<Thought>[]> = {}

thoughts.forEach((thought) => {
const date = new Date(thought.publishedAt)
const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
if (!groups[dateKey]) {
groups[dateKey] = []
}
groups[dateKey].push(thought)
})

return Object.entries(groups).sort((a, b) => b[0].localeCompare(a[0]))
})
</script>

<template>
<div flex="~ col gap-8">
<div v-for="[dateKey, dateThoughts] in groupedThoughts" :key="dateKey">
<h3 text="xl font-bold" mb-4>
{{ useDateFormat(() => dateKey, 'MMM DD YYYY', { locales: 'zh-Hans' }) }}
</h3>
<div flex="~ col gap-6">
<HomeThoughtItem
v-for="thought in dateThoughts"
:key="thought.id"
:thought="thought"
/>
</div>
</div>
</div>
</template>
11 changes: 0 additions & 11 deletions app/pages/(home)/about.vue

This file was deleted.

100 changes: 54 additions & 46 deletions app/pages/(home)/blog/[[page]].vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
<script lang="ts">
import type { SerializeObject } from 'nitropack/types'
import { defineColadaLoader } from 'unplugin-vue-router/data-loaders/pinia-colada'

const POSTS_LIMIT = 5

function normalizPage(page: string | number | undefined) {
return page ? Number.parseInt(page as string) : 1
}

export const usePostsData = defineColadaLoader('blog-page', {
key: to => ['posts', normalizPage(to.params.page)],
async query(to, { signal }) {
const page = normalizPage(to.params.page)
return $fetch<SerializeObject<{
data: Post[]
total: number
limit: number
offset: number
}>>('/api/post', {
query: {
limit: POSTS_LIMIT,
offset: (Math.max(1, page) - 1) * POSTS_LIMIT,
},
signal,
})
},
staleTime: 10 * 1000,
})
</script>

<script setup lang="ts">
import type { UsePaginationProps } from '@ark-ui/vue/pagination'
import { Pagination, usePagination } from '@ark-ui/vue/pagination'

defineOptions({
__loaders: [usePostsData],
})

definePageMeta({
alias: '/blog/:page(\\d+)?',
layout: 'home',
Expand All @@ -10,15 +45,9 @@ definePageMeta({
const route = useRoute('blog-page')
const router = useRouter()

const POSTS_LIMIT = 5
const page = computed<number>(() => route.params.page ? Number.parseInt(route.params.page as string) : 1)
const page = computed<number>(() => normalizPage(route.params.page))

const { data: posts } = await useFetch('/api/post', {
query: {
limit: POSTS_LIMIT,
offset: (Math.max(1, page.value) - 1) * POSTS_LIMIT,
},
})
const posts = await usePostsData()

const paginationOptions = computed<UsePaginationProps>(() => ({
onPageChange(details) {
Expand All @@ -29,8 +58,8 @@ const paginationOptions = computed<UsePaginationProps>(() => ({
},
})
},
count: posts.value ? posts.value.total : 0,
pageSize: posts.value?.limit ?? POSTS_LIMIT,
count: posts ? posts?.total : 0,
pageSize: posts?.limit ?? POSTS_LIMIT,
page: page.value,
siblingCount: 2,
}))
Expand All @@ -43,7 +72,7 @@ const pagination = usePagination(paginationOptions)
title="全部文章"
description="一般写博客文章是随心所欲的,想到什么就有可能会写一些,会希望能够把好用的技术知识传递给更多的人。喜欢围绕着技术为主的话题,但是也会写一些非技术的奇奇怪怪的话题。"
>
<div v-if="posts && posts?.data?.length > 0 " pl="md:6" border="md:l border" w-full>
<div v-if="posts && posts?.data?.length > 0" pl="md:6" border="md:l border" w-full>
<ul flex="~ col gap-16" pb-16>
<li v-for="(article, index) in posts?.data" :key="article.slug">
<Article :article="article" :delay="index * 0.1" />
Expand All @@ -54,19 +83,11 @@ const pagination = usePagination(paginationOptions)
No Posts
</div>

<Pagination.RootProvider
flex="~ row items-center justify-between gap-1"
w-full pt-4
:value="pagination"
>
<Pagination.RootProvider flex="~ row items-center justify-between gap-1" w-full pt-4 :value="pagination">
<Pagination.PrevTrigger
flex="inline items-center justify-center"
un-text="sm"
border="focus-visible:accent/50 rounded-md"
ring="focus-visible:accent/50 focus-visible:3px"
bg="hover:zinc-200/70 dark:hover:zinc-800/70"
p="y2 x2.5"
h-9 whitespace-nowrap font-medium outline-none
flex="inline items-center justify-center" un-text="sm"
border="focus-visible:accent/50 rounded-md" ring="focus-visible:accent/50 focus-visible:3px"
bg="hover:zinc-200/70 dark:hover:zinc-800/70" p="y2 x2.5" h-9 whitespace-nowrap font-medium outline-none
class="transition-all disabled:pointer-events-none disabled:cursor-not-allowed disabled:op-50"
>
上一页
Expand All @@ -75,42 +96,26 @@ const pagination = usePagination(paginationOptions)
<Pagination.Context v-slot="pagination">
<template v-for="(page, index) in pagination.pages">
<Pagination.Item
v-if="page.type === 'page'"
:key="index"
:value="page.value"
:type="page.type"
flex="inline items-center justify-center"
un-text="sm"
v-if="page.type === 'page'" :key="index" :value="page.value" :type="page.type"
flex="inline items-center justify-center" un-text="sm"
border="data-[selected]:~ data-[selected]:border focus-visible:accent/50 rounded-md"
ring="focus-visible:accent/50 focus-visible:3px"
bg="hover:zinc-200/70 dark:hover:zinc-800/70"
p="y2 x2.5"
ring="focus-visible:accent/50 focus-visible:3px" bg="hover:zinc-200/70 dark:hover:zinc-800/70" p="y2 x2.5"
size-9 whitespace-nowrap font-medium outline-none
class="transition-all disabled:pointer-events-none disabled:cursor-not-allowed disabled:op-50 data-[selected]:shadow-md"
>
{{ page.value }}
</Pagination.Item>
<Pagination.Ellipsis
v-else
:key="`e${index}`"
flex="~ items-center justify-center"
size-9
:index="index"
>
<Pagination.Ellipsis v-else :key="`e${index}`" flex="~ items-center justify-center" size-9 :index="index">
&#8230;
<span class="sr-only">更多页面</span>
</Pagination.Ellipsis>
</template>
</Pagination.Context>
</div>
<Pagination.NextTrigger
flex="inline items-center justify-center"
un-text="sm"
border="focus-visible:accent/50 rounded-md"
ring="focus-visible:accent/50 focus-visible:3px"
bg="hover:zinc-200/70 dark:hover:zinc-800/70"
p="y2 x2.5"
h-9 whitespace-nowrap font-medium outline-none
flex="inline items-center justify-center" un-text="sm"
border="focus-visible:accent/50 rounded-md" ring="focus-visible:accent/50 focus-visible:3px"
bg="hover:zinc-200/70 dark:hover:zinc-800/70" p="y2 x2.5" h-9 whitespace-nowrap font-medium outline-none
class="transition-all disabled:pointer-events-none disabled:cursor-not-allowed disabled:op-50"
>
下一页
Expand All @@ -126,6 +131,7 @@ const pagination = usePagination(paginationOptions)
display: inline-block;
position: relative;
}

.loader::after,
.loader::before {
content: '';
Expand All @@ -139,6 +145,7 @@ const pagination = usePagination(paginationOptions)
top: 0;
animation: animloader 2s linear infinite;
}

.loader::after {
animation-delay: 1s;
}
Expand All @@ -148,6 +155,7 @@ const pagination = usePagination(paginationOptions)
transform: scale(0);
opacity: 1;
}

100% {
transform: scale(1);
opacity: 0;
Expand Down
31 changes: 25 additions & 6 deletions app/pages/(home)/blog/[slug].vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
<script lang="ts">
import { defineColadaLoader } from 'unplugin-vue-router/data-loaders/pinia-colada'

export const usePostData = defineColadaLoader('blog-slug', {
key: to => ['post', to.params.slug],
async query(to, { signal }) {
return $fetch(`/api/post/${to.params.slug}`, { signal })
},
staleTime: 10 * 1000,
})

export const useSurroundPostData = defineColadaLoader('blog-slug', {
key: to => ['post', to.params.slug, 'surround'],
async query(to, { signal }) {
return $fetch(`/api/post/${to.params.slug}/surround`, { signal })
},
staleTime: 10 * 1000,
})
</script>

<script setup lang="ts">
defineOptions({
name: 'BlogSlug',
__loaders: [usePostData, useSurroundPostData],
})

definePageMeta({
layout: 'home',
})

const route = useRoute('blog-slug')

const { data } = await useFetch(`/api/post/${route.params.slug}`)
const data = await usePostData()

const { data: surroundData } = await useFetch(`/api/post/${route.params.slug}/surround`)
const surroundData = await useSurroundPostData()

useHead({
title: computed(() => data.value?.title),
title: computed(() => data?.title),
})

if (!data.value) {
if (!data) {
throw createError({
statusCode: 404,
statusMessage: 'Post not found',
Expand Down
Loading