casualroom/apps/fenghuo/web/components/articles/article-row.tsx

207 lines
6.7 KiB
TypeScript
Executable File

import dayjs from 'dayjs';
import { Badge } from '@nice/ui/components/badge';
import { Button } from '@nice/ui/components/button';
import { Checkbox } from '@nice/ui/components/checkbox';
import { TableCell, TableRow } from '@nice/ui/components/table';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@nice/ui/components/dropdown-menu';
import {
IconClock,
IconEye,
IconEyeOff,
IconFileText,
IconDotsVertical,
IconEdit,
IconCopy,
IconShare,
IconArchive,
IconRestore,
IconTrash,
IconExternalLink,
} from '@tabler/icons-react';
import { ArticleStatusBadge } from './article-status-badge';
import { formatNumber } from '@/lib/articles/utils';
import { useArticlesContext } from './context';
import type { Article } from '@fenghuo/common';
interface ArticleRowProps {
article: Article;
}
export function ArticleRow({ article }: ArticleRowProps) {
const { selectedArticles, handleSelectArticle, setQuickEditId, handleArticleAction, updateVisibility } =
useArticlesContext();
const isSelected = selectedArticles.includes(article.id);
const onSelect = (checked: boolean) => {
handleSelectArticle(article.id, checked);
};
const onQuickEdit = () => {
setQuickEditId(article.id);
};
return (
<TableRow className="group hover:bg-muted/20 transition-colors border-b border-border/50">
<TableCell className="py-4">
<Checkbox checked={isSelected} onCheckedChange={onSelect} />
</TableCell>
<TableCell className="py-4">
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-start gap-2">
<h3 className="font-medium text-sm leading-5 hover:text-primary cursor-pointer transition-colors line-clamp-2">
{article.title}
</h3>
</div>
{article.excerpt && (
<p className="text-xs text-muted-foreground leading-4 line-clamp-2">{article.excerpt}</p>
)}
<div className="flex items-center gap-4 pt-1">
<button
onClick={onQuickEdit}
className="text-xs text-muted-foreground hover:text-primary transition-colors"
>
</button>
<button
onClick={() => handleArticleAction(article.id, 'view')}
className="text-xs text-muted-foreground hover:text-primary transition-colors"
>
</button>
{article.status === 'published' && (
<button
onClick={() => handleArticleAction(article.id, 'preview')}
className="text-xs text-muted-foreground hover:text-primary transition-colors flex items-center gap-1"
>
<IconExternalLink className="h-3 w-3" />
</button>
)}
</div>
</div>
</div>
</TableCell>
<TableCell className="py-4 align-top">
<ArticleStatusBadge status={article.status} />
</TableCell>
<TableCell className="py-4 align-top">
{article.terms?.[0]?.name && (
<Badge variant="secondary" className="text-xs px-2 py-1 bg-secondary/50">
{article.terms?.[0]?.name}
</Badge>
)}
</TableCell>
<TableCell className="py-4 align-top">
<div className="space-y-1">
<div className="text-sm font-medium leading-4">{article.author?.username}</div>
<div className="text-xs text-muted-foreground leading-3">{article.organization?.name}</div>
</div>
</TableCell>
<TableCell className="py-4 align-top">
<div className="space-y-1.5">
{article.viewCount > 0 && (
<div className="flex items-center gap-1.5 text-xs">
<IconEye className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground">{formatNumber(article.viewCount)}</span>
</div>
)}
{article.commentCount > 0 && (
<div className="flex items-center gap-1.5 text-xs">
<IconFileText className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground">{formatNumber(article.commentCount)}</span>
</div>
)}
</div>
</TableCell>
<TableCell className="py-4 align-top">
<div className="space-y-1.5">
<div className="flex items-center gap-1.5 text-xs">
<IconClock className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground">{dayjs(article.createdAt).format('YYYY-MM-DD HH:mm')}</span>
</div>
{article.publishedAt && (
<div className="flex items-center gap-1.5 text-xs">
<div className="w-2 h-2 bg-primary rounded-full"></div>
<span className="text-primary"></span>
</div>
)}
</div>
</TableCell>
<TableCell className="py-4 align-top">
<div className="flex items-center justify-center gap-1">
{/* 可见性切换 */}
{article.visibility === 'public' ? (
<Button
size="sm"
variant="outline"
onClick={() => updateVisibility(article.id, 'private')}
className="h-8 w-8 p-0"
title="设为私密"
>
<IconEye className="h-3 w-3" />
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={() => updateVisibility(article.id, 'public')}
className="h-8 w-8 p-0"
title="设为公开"
>
<IconEyeOff className="h-3 w-3" />
</Button>
)}
{/* 更多操作 */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
<IconDotsVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem onClick={() => handleArticleAction(article.id, 'edit')} className="cursor-pointer">
<IconEdit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleArticleAction(article.id, 'duplicate')} className="cursor-pointer">
<IconCopy className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuSeparator />
{article.status === 'archived' ? (
<DropdownMenuItem onClick={() => handleArticleAction(article.id, 'restore')} className="cursor-pointer">
<IconRestore className="h-4 w-4 mr-2" />
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={() => handleArticleAction(article.id, 'archive')} className="cursor-pointer">
<IconArchive className="h-4 w-4 mr-2" />
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive cursor-pointer"
onClick={() => handleArticleAction(article.id, 'delete')}
>
<IconTrash className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
);
}