geo/frontend/components/dashboard/SuggestionCard.tsx

354 lines
10 KiB
TypeScript

"use client";
import React, { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import {
Suggestion,
SuggestionType,
SUGGESTION_TYPE_CONFIG,
PRIORITY_CONFIG,
DIFFICULTY_CONFIG,
STATUS_CONFIG,
} from "@/types/suggestion";
import {
FileText,
Monitor,
GitCompare,
Search,
Quote,
ChevronDown,
ChevronUp,
Check,
X,
Play,
RefreshCw,
Loader2,
Lightbulb,
} from "lucide-react";
// 类型图标映射
const TYPE_ICONS: Record<SuggestionType, React.ReactNode> = {
content_optimization: <FileText className="h-4 w-4" />,
platform_targeting: <Monitor className="h-4 w-4" />,
competitor_gap: <GitCompare className="h-4 w-4" />,
query_expansion: <Search className="h-4 w-4" />,
citation_improvement: <Quote className="h-4 w-4" />,
};
interface SuggestionCardProps {
suggestion: Suggestion;
brandId: string;
onStatusChange?: (suggestionId: string, newStatus: string) => void;
compact?: boolean;
}
/**
* 单条优化建议卡片组件
*/
export function SuggestionCard({
suggestion,
brandId: _brandId,
onStatusChange,
compact = false,
}: SuggestionCardProps) {
const [expanded, setExpanded] = useState(false);
const [loading, setLoading] = useState(false);
const typeConfig = SUGGESTION_TYPE_CONFIG[suggestion.type];
const priorityConfig = PRIORITY_CONFIG[suggestion.priority];
const difficultyConfig = DIFFICULTY_CONFIG[suggestion.difficulty];
const statusConfig = STATUS_CONFIG[suggestion.status];
const handleStatusChange = async (newStatus: string) => {
setLoading(true);
try {
await onStatusChange?.(suggestion.id, newStatus);
} finally {
setLoading(false);
}
};
const isCompleted = suggestion.status === "completed";
const isDismissed = suggestion.status === "dismissed";
const _isInProgress = suggestion.status === "in_progress";
return (
<div
className={cn(
"rounded-lg border-l-4 border bg-card p-4 transition-all",
priorityConfig.borderColor,
isCompleted && "opacity-60",
isDismissed && "opacity-40",
!compact && "hover:shadow-md",
)}
>
<div className="flex items-start gap-3">
{/* 类型图标 */}
<div
className={cn(
"mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md",
typeConfig.bgColor,
typeConfig.color,
)}
>
{TYPE_ICONS[suggestion.type]}
</div>
{/* 内容区域 */}
<div className="flex-1 min-w-0">
{/* 标题行 */}
<div className="flex items-center gap-2 mb-1 flex-wrap">
<Badge variant="outline" className={cn("text-xs", typeConfig.color, typeConfig.bgColor, "border-0")}>
{typeConfig.label}
</Badge>
<Badge variant="outline" className={cn("text-xs", priorityConfig.color, priorityConfig.bgColor, "border-0")}>
{priorityConfig.label}
</Badge>
<Badge variant="outline" className={cn("text-xs", statusConfig.color, statusConfig.bgColor, "border-0")}>
{statusConfig.label}
</Badge>
{suggestion.source === "llm" && (
<Badge variant="outline" className="text-xs text-violet-600 bg-violet-50 border-0">
AI生成
</Badge>
)}
</div>
{/* 标题 */}
<h4
className={cn(
"font-medium text-sm",
isCompleted && "line-through text-muted-foreground",
)}
>
{suggestion.title}
</h4>
{/* 描述 */}
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
{suggestion.description}
</p>
{/* 展开区域 */}
{expanded && (
<div className="mt-3 space-y-3 text-sm">
{/* 操作步骤 */}
{suggestion.action && (
<div>
<h5 className="font-medium text-foreground mb-1"></h5>
<div className="whitespace-pre-line text-muted-foreground bg-muted/50 rounded-md p-3">
{suggestion.action}
</div>
</div>
)}
{/* 预期效果 */}
{suggestion.expected_impact && (
<div>
<h5 className="font-medium text-foreground mb-1"></h5>
<p className="text-muted-foreground">{suggestion.expected_impact}</p>
</div>
)}
{/* 难度 */}
<div className="flex items-center gap-2">
<span className="text-muted-foreground">:</span>
<span className={cn("font-medium", difficultyConfig.color)}>
{difficultyConfig.label}
</span>
</div>
</div>
)}
{/* 操作按钮 */}
<div className="mt-3 flex items-center gap-2">
{/* 展开/收起 */}
{!compact && (suggestion.action || suggestion.expected_impact) && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<>
<ChevronUp className="mr-1 h-3 w-3" />
</>
) : (
<>
<ChevronDown className="mr-1 h-3 w-3" />
</>
)}
</Button>
)}
{/* 状态操作按钮 */}
{suggestion.status === "pending" && (
<Button
variant="outline"
size="sm"
className="h-7 text-xs text-blue-600 border-blue-200 bg-blue-50 hover:bg-blue-100"
disabled={loading}
onClick={() => handleStatusChange("in_progress")}
>
{loading ? (
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
) : (
<Play className="mr-1 h-3 w-3" />
)}
</Button>
)}
{suggestion.status === "in_progress" && (
<Button
variant="outline"
size="sm"
className="h-7 text-xs text-emerald-600 border-emerald-200 bg-emerald-50 hover:bg-emerald-100"
disabled={loading}
onClick={() => handleStatusChange("completed")}
>
{loading ? (
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
) : (
<Check className="mr-1 h-3 w-3" />
)}
</Button>
)}
{suggestion.status === "pending" && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-muted-foreground"
disabled={loading}
onClick={() => handleStatusChange("dismissed")}
>
<X className="mr-1 h-3 w-3" />
</Button>
)}
</div>
</div>
</div>
</div>
);
}
interface SuggestionListProps {
suggestions: Suggestion[];
brandId: string;
onStatusChange?: (suggestionId: string, newStatus: string) => void;
loading?: boolean;
onRegenerate?: () => void;
className?: string;
compact?: boolean;
maxItems?: number;
}
/**
* 优化建议列表组件
*/
export function SuggestionList({
suggestions,
brandId,
onStatusChange,
loading = false,
onRegenerate,
className,
compact = false,
maxItems,
}: SuggestionListProps) {
const displaySuggestions = maxItems
? suggestions.slice(0, maxItems)
: suggestions;
if (loading) {
return (
<Card className={className}>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Lightbulb className="h-5 w-5 text-primary" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div
key={i}
className="h-24 animate-pulse rounded-lg bg-muted"
/>
))}
</div>
</CardContent>
</Card>
);
}
return (
<Card className={className}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base flex items-center gap-2">
<Lightbulb className="h-5 w-5 text-primary" />
{suggestions.length > 0 && (
<Badge variant="secondary" className="ml-1">
{suggestions.length}
</Badge>
)}
</CardTitle>
{onRegenerate && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={onRegenerate}
>
<RefreshCw className="mr-1 h-3 w-3" />
</Button>
)}
</div>
</CardHeader>
<CardContent>
{displaySuggestions.length === 0 ? (
<div className="flex h-[100px] flex-col items-center justify-center text-muted-foreground">
<Lightbulb className="mb-2 h-8 w-8" />
<p className="text-sm"></p>
</div>
) : (
<div className="space-y-3">
{displaySuggestions.map((suggestion) => (
<SuggestionCard
key={suggestion.id}
suggestion={suggestion}
brandId={brandId}
onStatusChange={onStatusChange}
compact={compact}
/>
))}
{maxItems && suggestions.length > maxItems && (
<div className="text-center">
<Button variant="link" size="sm" className="text-xs" asChild>
<a href="/dashboard/suggestions">
{suggestions.length}
</a>
</Button>
</div>
)}
</div>
)}
</CardContent>
</Card>
);
}