207 lines
6.7 KiB
TypeScript
Executable File
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>
|
|
);
|
|
}
|