354 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|