198 lines
5.7 KiB
TypeScript
198 lines
5.7 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import Link from "next/link";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import type { ActionItemProps, NextAction, NextActionCardProps } from "@/types/next-action";
|
|
import { Target, Lightbulb, Bell } from "lucide-react";
|
|
|
|
// 优先级图标映射
|
|
const PRIORITY_ICONS: Record<NextAction["priority"], React.ReactNode> = {
|
|
primary: <Target className="h-5 w-5" />,
|
|
secondary: <Lightbulb className="h-5 w-5" />,
|
|
optional: <Bell className="h-5 w-5" />,
|
|
};
|
|
|
|
// 优先级标签映射
|
|
const PRIORITY_LABELS: Record<NextAction["priority"], string> = {
|
|
primary: "主要",
|
|
secondary: "次要",
|
|
optional: "可选",
|
|
};
|
|
|
|
// 优先级颜色配置
|
|
const PRIORITY_COLORS: Record<NextAction["priority"], { border: string; bg: string; icon: string }> = {
|
|
primary: {
|
|
border: "border-l-4 border-l-orange-500 border-orange-200",
|
|
bg: "bg-orange-50",
|
|
icon: "text-orange-500",
|
|
},
|
|
secondary: {
|
|
border: "border-l-4 border-l-blue-500 border-blue-200",
|
|
bg: "bg-blue-50",
|
|
icon: "text-blue-500",
|
|
},
|
|
optional: {
|
|
border: "border-l-4 border-l-gray-400 border-gray-200",
|
|
bg: "bg-gray-50",
|
|
icon: "text-gray-500",
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 单个行动项组件
|
|
*/
|
|
function ActionItem({ action, onClick }: ActionItemProps) {
|
|
const colors = PRIORITY_COLORS[action.priority];
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"rounded-lg border p-4 transition-all hover:shadow-md",
|
|
colors.border,
|
|
)}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
{/* 优先级图标 */}
|
|
<div className={cn("mt-0.5", colors.icon)}>
|
|
{PRIORITY_ICONS[action.priority]}
|
|
</div>
|
|
|
|
{/* 内容区域 */}
|
|
<div className="flex-1 min-w-0">
|
|
{/* 标题和描述 */}
|
|
<div className="mb-2">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-xs font-medium text-muted-foreground">
|
|
{PRIORITY_LABELS[action.priority]}
|
|
</span>
|
|
<span className="text-sm">{action.icon}</span>
|
|
</div>
|
|
<h4 className="font-medium text-sm">{action.title}</h4>
|
|
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
{action.description}
|
|
</p>
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div className="mt-3">
|
|
{action.actionUrl.startsWith("/") ? (
|
|
<Link href={action.actionUrl}>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className={cn(
|
|
"text-xs h-7",
|
|
action.priority === "primary" &&
|
|
"border-orange-300 bg-orange-100 hover:bg-orange-200 text-orange-700",
|
|
action.priority === "secondary" &&
|
|
"border-blue-300 bg-blue-100 hover:bg-blue-200 text-blue-700",
|
|
)}
|
|
onClick={onClick}
|
|
>
|
|
{action.actionText}
|
|
<span className="ml-1">→</span>
|
|
</Button>
|
|
</Link>
|
|
) : (
|
|
<a
|
|
href={action.actionUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-xs h-7"
|
|
onClick={onClick}
|
|
>
|
|
{action.actionText}
|
|
<span className="ml-1">→</span>
|
|
</Button>
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 下一步行动建议卡片组件
|
|
*/
|
|
export function NextActionCard({ context, className, onActionClick }: NextActionCardProps) {
|
|
// 动态导入以避免循环依赖
|
|
const [actions, setActions] = React.useState<NextAction[]>([]);
|
|
const [loading, setLoading] = React.useState(true);
|
|
|
|
React.useEffect(() => {
|
|
async function loadActions() {
|
|
try {
|
|
const { generateNextActions } = await import("@/lib/next-action");
|
|
const generatedActions = generateNextActions(context);
|
|
setActions(generatedActions);
|
|
} catch (error) {
|
|
console.error("生成行动建议失败:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
loadActions();
|
|
}, [context]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Target 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>
|
|
);
|
|
}
|
|
|
|
if (actions.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Target className="h-5 w-5 text-primary" />
|
|
为您推荐的下一步行动
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{actions.map((action) => (
|
|
<ActionItem
|
|
key={action.id}
|
|
action={action}
|
|
onClick={() => onActionClick?.(action)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export { ActionItem };
|