laobinghu ce208df092 feat: 实现 Nuxt4 + Nuxt UI 博客前端完整功能
核心功能:
- 项目初始化 (Nuxt 4 + Nuxt UI + Pinia + ofetch)
- TypeScript 类型定义 (User, Article, Comment, API 响应)
- 认证系统 (登录/登出、Cookie 支持、权限中间件)
- 文章列表页 (筛选、分页、响应式布局)
- 文章详情页 (Markdown 渲染、评论系统)
- 文章编辑器 (左右分栏、实时预览、Markdown 工具栏)

管理后台:
- 侧边栏布局、权限检查
- 数据分析 (数据统计卡片、热门文章、评论审核统计)
- 文章管理 (表格、筛选、删除)
- 评论管理 (审核通过/拒绝、删除)
- 用户管理 (角色管理、删除)

全局组件:
- 导航栏 (暗色模式切换、移动端菜单)
- 页脚
- 403/404 错误页

配置文件:
- .env.example 环境变量模板
- nuxt.config.ts 完整配置
- 自定义 CSS 样式
2026-03-28 15:56:50 +08:00

124 lines
2.8 KiB
Vue

<template>
<div>
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">
最新文章
</h1>
<div class="flex flex-col sm:flex-row gap-4">
<UInput
v-model="search"
placeholder="搜索文章..."
icon="i-heroicons-magnifying-glass"
class="flex-1"
/>
<USelect v-model="statusFilter" :options="statusOptions" />
</div>
</div>
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<USkeleton v-for="i in 6" :key="i" class="h-64" />
</div>
<div
v-else-if="articles.length === 0"
class="text-center py-16"
>
<UIcon
name="i-heroicons-document-text"
class="w-16 h-16 mx-auto text-gray-300 dark:text-gray-600 mb-4"
/>
<p class="text-gray-600 dark:text-gray-400">
{{ search || statusFilter ? '没有找到相关文章' : '暂无文章' }}
</p>
</div>
<div
v-else
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
>
<ArticleCard
v-for="article in articles"
:key="article.id"
:article="article"
/>
</div>
<div v-if="totalPages > 1" class="mt-8 flex justify-center">
<UPagination
v-model="page"
:total="total"
:page-size="limit"
:max="7"
@update:model-value="onPageChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'default',
})
useSeoMeta({
title: '首页 - LinkShare Blog',
description: 'LinkShare Blog - 分享技术与生活',
ogTitle: 'LinkShare Blog',
ogDescription: '分享技术与生活',
ogType: 'website',
})
const { fetchArticles } = useArticles()
const route = useRouter()
const search = useRouteQuery('search', '')
const statusFilter = useRouteQuery<string>('status', '')
const page = useRouteQuery<number>('page', 1)
const limit = 20
const articles = ref<any[]>([])
const total = ref(0)
const totalPages = ref(0)
const loading = ref(true)
const statusOptions = [
{ label: '全部状态', value: '' },
{ label: '已发布', value: 'published' },
{ label: '草稿', value: 'draft' },
{ label: '归档', value: 'archived' },
]
const loadArticles = async () => {
loading.value = true
try {
const response = await fetchArticles({
page: page.value,
limit,
status: statusFilter.value,
search: search.value,
})
articles.value = response.items
total.value = response.total
totalPages.value = response.totalPages
} catch (error) {
console.error('Failed to load articles:', error)
} finally {
loading.value = false
}
}
const onPageChange = (newPage: number) => {
page.value = newPage
}
watch([search, statusFilter, page], () => {
loadArticles()
})
onMounted(() => {
loadArticles()
})
</script>