Major changes: - Shift from "OpenFang desktop client" to "independent AI Agent desktop app" - Add decision principle: "Is this useful for ZCLAW? Does it affect ZCLAW?" - Simplify project structure and tech stack sections - Replace OpenClaw vs OpenFang comparison with unified backend approach - Consolidate troubleshooting from scattered sections into organized FAQ - Update Hands system documentation with 8 capabilities and status - Stream
212 lines
7.0 KiB
TypeScript
212 lines
7.0 KiB
TypeScript
/**
|
|
* * SkillCard - 技能卡片组件
|
|
*
|
|
* * 展示单个技能的基本信息,包括名称、描述、能力和安装状态
|
|
*/
|
|
|
|
import { motion } from 'framer-motion';
|
|
import {
|
|
Package,
|
|
Check,
|
|
Star,
|
|
MoreHorizontal,
|
|
Clock,
|
|
} from 'lucide-react';
|
|
import type { Skill } from '../../types/skill-market';
|
|
import { useState } from 'react';
|
|
|
|
// === 类型定义 ===
|
|
|
|
interface SkillCardProps {
|
|
/** 技能数据 */
|
|
skill: Skill;
|
|
/** 是否选中 */
|
|
isSelected?: boolean;
|
|
/** 点击回调 */
|
|
onClick?: () => void;
|
|
/** 安装/卸载回调 */
|
|
onToggleInstall?: () => void;
|
|
/** 显示更多操作 */
|
|
onShowMore?: () => void;
|
|
}
|
|
|
|
// === 分类配置 ===
|
|
|
|
const CATEGORY_CONFIG: Record<string, { color: string; bgColor: string }> = {
|
|
development: { color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-100 dark:bg-blue-900/30' },
|
|
security: { color: 'text-red-600 dark:text-red-400', bgColor: 'bg-red-100 dark:bg-red-900/30' },
|
|
analytics: { color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/30' },
|
|
content: { color: 'text-pink-600 dark:text-pink-400', bgColor: 'bg-pink-100 dark:bg-pink-900/30' },
|
|
ops: { color: 'text-orange-600 dark:text-orange-400', bgColor: 'bg-orange-100 dark:bg-orange-900/30' },
|
|
management: { color: 'text-cyan-600 dark:text-cyan-400', bgColor: 'bg-cyan-100 dark:bg-cyan-900/30' },
|
|
testing: { color: 'text-emerald-600 dark:text-emerald-400', bgColor: 'bg-emerald-100 dark:bg-emerald-900/30' },
|
|
business: { color: 'text-amber-600 dark:text-amber-400', bgColor: 'bg-amber-100 dark:bg-amber-900/30' },
|
|
marketing: { color: 'text-rose-600 dark:text-rose-400', bgColor: 'bg-rose-100 dark:bg-rose-900/30' },
|
|
};
|
|
|
|
// === 分类名称映射 ===
|
|
|
|
const CATEGORY_NAMES: Record<string, string> = {
|
|
development: '开发',
|
|
security: '安全',
|
|
analytics: '分析',
|
|
content: '内容',
|
|
ops: '运维',
|
|
management: '管理',
|
|
testing: '测试',
|
|
business: '商务',
|
|
marketing: '营销',
|
|
};
|
|
|
|
/**
|
|
* SkillCard - 技能卡片组件
|
|
*/
|
|
export function SkillCard({
|
|
skill,
|
|
isSelected = false,
|
|
onClick,
|
|
onToggleInstall,
|
|
onShowMore,
|
|
}: SkillCardProps) {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const categoryConfig = CATEGORY_CONFIG[skill.category] || {
|
|
color: 'text-gray-600 dark:text-gray-400',
|
|
bgColor: 'bg-gray-100 dark:bg-gray-800/30',
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
whileHover={{ scale: 1.02 }}
|
|
onHoverStart={() => setIsHovered(true)}
|
|
onHoverEnd={() => setIsHovered(false)}
|
|
onClick={onClick}
|
|
className={`
|
|
relative p-4 rounded-lg border cursor-pointer transition-all duration-200
|
|
${isSelected
|
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
|
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-gray-300 dark:hover:border-gray-600'
|
|
}
|
|
`}
|
|
>
|
|
{/* 顶部:图标和名称 */}
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`w-10 h-10 rounded-lg flex items-center justify-center ${categoryConfig.bgColor}`}
|
|
>
|
|
<Package className={`w-5 h-5 ${categoryConfig.color}`} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
{skill.name}
|
|
</h3>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
{skill.author || '官方'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 安装按钮 */}
|
|
<motion.button
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleInstall?.();
|
|
}}
|
|
className={`
|
|
px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200
|
|
${skill.installed
|
|
? 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
: 'bg-gray-700 dark:bg-gray-600 text-white hover:bg-gray-800 dark:hover:bg-gray-500'
|
|
}
|
|
`}
|
|
>
|
|
{skill.installed ? (
|
|
<span className="flex items-center gap-1">
|
|
<Check className="w-3 h-3" />
|
|
已安装
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-1">
|
|
<Package className="w-3 h-3" />
|
|
安装
|
|
</span>
|
|
)}
|
|
</motion.button>
|
|
</div>
|
|
|
|
{/* 描述 */}
|
|
<p className="text-xs text-gray-600 dark:text-gray-300 mb-3 line-clamp-2">
|
|
{skill.description}
|
|
</p>
|
|
|
|
{/* 标签和能力 */}
|
|
<div className="flex flex-wrap gap-1 mb-3">
|
|
{skill.capabilities.slice(0, 3).map((cap) => (
|
|
<span
|
|
key={cap}
|
|
className="px-2 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
|
|
>
|
|
{cap}
|
|
</span>
|
|
))}
|
|
{skill.capabilities.length > 3 && (
|
|
<span className="px-2 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
|
+{skill.capabilities.length - 3}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* 底部:分类、评分和统计 */}
|
|
<div className="flex items-center justify-between pt-2 border-t border-gray-100 dark:border-gray-700">
|
|
<span
|
|
className={`px-2 py-0.5 rounded text-xs ${categoryConfig.bgColor} ${categoryConfig.color}`}
|
|
>
|
|
{CATEGORY_NAMES[skill.category] || skill.category}
|
|
</span>
|
|
|
|
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
|
{skill.rating !== undefined && (
|
|
<span className="flex items-center gap-1">
|
|
<Star className="w-3 h-3 text-yellow-500 fill-current" />
|
|
{skill.rating.toFixed(1)}
|
|
</span>
|
|
)}
|
|
{skill.reviewCount !== undefined && skill.reviewCount > 0 && (
|
|
<span>{skill.reviewCount} 评价</span>
|
|
)}
|
|
{skill.installedAt && (
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{new Date(skill.installedAt).toLocaleDateString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 悬停时显示更多按钮 */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: isHovered ? 1 : 0 }}
|
|
className="absolute top-2 right-2"
|
|
>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onShowMore?.();
|
|
}}
|
|
className="p-1.5 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
title="更多操作"
|
|
>
|
|
<MoreHorizontal className="w-4 h-4" />
|
|
</button>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
export default SkillCard;
|