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

119 lines
3.1 KiB
Vue

<template>
<UCard class="h-full flex flex-col">
<NuxtLink
v-if="article.coverImageUrl"
:to="`/article/${article.slug}`"
class="block -mx-4 -mt-4 mb-4"
>
<img
:src="article.coverImageUrl"
:alt="article.title"
class="w-full h-48 object-cover rounded-t-lg"
loading="lazy"
/>
</NuxtLink>
<div class="flex-1 flex flex-col">
<div class="flex items-center justify-between mb-2">
<UBadge
:label="statusLabels[article.status]"
:color="statusColors[article.status]"
size="xs"
variant="subtle"
/>
<div class="flex items-center text-gray-500 dark:text-gray-400 text-xs">
<UIcon name="i-heroicons-eye" class="w-4 h-4 mr-1" />
{{ formatNumber(article.viewCount) }}
</div>
</div>
<NuxtLink :to="`/article/${article.slug}`" class="group">
<h3
class="text-lg font-semibold text-gray-900 dark:text-white mb-2 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors line-clamp-2"
>
{{ article.title }}
</h3>
</NuxtLink>
<p
v-if="article.excerpt"
class="text-gray-600 dark:text-gray-400 text-sm mb-4 line-clamp-3 flex-1"
>
{{ article.excerpt }}
</p>
<div class="flex items-center justify-between pt-4 border-t border-gray-100 dark:border-gray-700">
<div class="flex items-center">
<UAvatar
:src="article.author.avatarUrl"
:alt="article.author.displayName || article.author.username"
size="2xs"
class="mr-2"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">
{{ article.author.displayName || article.author.username }}
</span>
</div>
<time
:datetime="article.createdAt"
class="text-xs text-gray-500 dark:text-gray-400"
>
{{ timeAgo(article.createdAt) }}
</time>
</div>
</div>
</UCard>
</template>
<script setup lang="ts">
import type { Article } from '~/types/models'
defineProps<{
article: Article
}>()
const statusLabels: Record<string, string> = {
published: '已发布',
draft: '草稿',
archived: '归档',
}
const statusColors: Record<string, string> = {
published: 'green',
draft: 'yellow',
archived: 'gray',
}
const formatNumber = (num: number) => {
if (num >= 10000) {
return `${(num / 10000).toFixed(1)}w`
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`
}
return num.toString()
}
const timeAgo = (date: string) => {
const now = new Date()
const past = new Date(date)
const diff = now.getTime() - past.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) {
return '今天'
} else if (days === 1) {
return '昨天'
} else if (days < 7) {
return `${days}天前`
} else if (days < 30) {
return `${Math.floor(days / 7)}周前`
} else if (days < 365) {
return `${Math.floor(days / 30)}个月前`
} else {
return `${Math.floor(days / 365)}年前`
}
}
</script>