核心功能: - 项目初始化 (Nuxt 4 + Nuxt UI + Pinia + ofetch) - TypeScript 类型定义 (User, Article, Comment, API 响应) - 认证系统 (登录/登出、Cookie 支持、权限中间件) - 文章列表页 (筛选、分页、响应式布局) - 文章详情页 (Markdown 渲染、评论系统) - 文章编辑器 (左右分栏、实时预览、Markdown 工具栏) 管理后台: - 侧边栏布局、权限检查 - 数据分析 (数据统计卡片、热门文章、评论审核统计) - 文章管理 (表格、筛选、删除) - 评论管理 (审核通过/拒绝、删除) - 用户管理 (角色管理、删除) 全局组件: - 导航栏 (暗色模式切换、移动端菜单) - 页脚 - 403/404 错误页 配置文件: - .env.example 环境变量模板 - nuxt.config.ts 完整配置 - 自定义 CSS 样式
124 lines
2.8 KiB
Vue
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>
|