Files
zclaw_openfang/desktop/src/components/TriggersPanel.tsx
iven 0d4fa96b82
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor: 统一项目名称从OpenFang到ZCLAW
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括:
- 配置文件中的项目名称
- 代码注释和文档引用
- 环境变量和路径
- 类型定义和接口名称
- 测试用例和模拟数据

同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
2026-03-27 07:36:03 +08:00

244 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* TriggersPanel - ZCLAW Triggers Management UI
*
* Displays available ZCLAW Triggers and allows creating and toggling them.
*/
import { useState, useEffect, useCallback } from 'react';
import { useHandStore } from '../store/handStore';
import type { Trigger } from '../store/handStore';
import { CreateTriggerModal } from './CreateTriggerModal';
import {
Zap,
RefreshCw,
Plus,
Globe,
Bell,
MessageSquare,
X,
} from 'lucide-react';
// === Trigger Type Config ===
const TRIGGER_TYPE_CONFIG: Record<string, { icon: typeof Zap; label: string; color: string }> = {
webhook: { icon: Globe, label: 'Webhook', color: 'text-blue-500' },
event: { icon: Bell, label: '事件', color: 'text-amber-500' },
message: { icon: MessageSquare, label: '消息', color: 'text-green-500' },
schedule: { icon: Zap, label: '定时', color: 'text-purple-500' },
file: { icon: Zap, label: '文件', color: 'text-cyan-500' },
manual: { icon: Zap, label: '手动', color: 'text-gray-500' },
};
interface TriggerCardProps {
trigger: Trigger;
onToggle: (id: string, enabled: boolean) => Promise<void>;
onDelete: (id: string) => Promise<void>;
isToggling: boolean;
isDeleting: boolean;
}
function TriggerCard({ trigger, onToggle, onDelete, isToggling, isDeleting }: TriggerCardProps) {
const handleToggle = async () => {
await onToggle(trigger.id, !trigger.enabled);
};
const handleDelete = async () => {
if (confirm(`确定要删除触发器 "${trigger.id}" 吗?`)) {
await onDelete(trigger.id);
}
};
const typeConfig = TRIGGER_TYPE_CONFIG[trigger.type] || { icon: Zap, label: trigger.type, color: 'text-gray-500' };
const TypeIcon = typeConfig.icon;
return (
<div className={`bg-white dark:bg-gray-800 rounded-lg border p-4 shadow-sm transition-colors ${
trigger.enabled
? 'border-green-200 dark:border-green-800'
: 'border-gray-200 dark:border-gray-700'
}`}>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<TypeIcon className={`w-4 h-4 ${typeConfig.color}`} />
<h3 className="font-medium text-gray-900 dark:text-white truncate">{trigger.id}</h3>
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${
trigger.enabled ? 'bg-green-500' : 'bg-gray-400'
}`} />
</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
{typeConfig.label}
</span>
<span className={`text-xs ${trigger.enabled ? 'text-green-600 dark:text-green-400' : 'text-gray-500 dark:text-gray-400'}`}>
{trigger.enabled ? '已启用' : '已禁用'}
</span>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={handleDelete}
disabled={isDeleting}
className="p-1.5 text-gray-400 hover:text-red-500 rounded-md disabled:opacity-50"
title="删除"
>
<X className="w-4 h-4" />
</button>
<button
onClick={handleToggle}
disabled={isToggling || isDeleting}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
trigger.enabled ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'
} ${(isToggling || isDeleting) ? 'opacity-50 cursor-not-allowed' : ''}`}
title={trigger.enabled ? '点击禁用' : '点击启用'}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
trigger.enabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
</div>
</div>
);
}
export function TriggersPanel() {
const triggers = useHandStore((s) => s.triggers);
const loadTriggers = useHandStore((s) => s.loadTriggers);
const updateTrigger = useHandStore((s) => s.updateTrigger);
const deleteTrigger = useHandStore((s) => s.deleteTrigger);
const isLoading = useHandStore((s) => s.isLoading);
const [togglingTrigger, setTogglingTrigger] = useState<string | null>(null);
const [deletingTrigger, setDeletingTrigger] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
useEffect(() => {
loadTriggers();
}, [loadTriggers]);
const handleToggle = useCallback(async (id: string, enabled: boolean) => {
setTogglingTrigger(id);
try {
await updateTrigger(id, { enabled });
await loadTriggers();
} catch (error) {
console.error('Failed to toggle trigger:', error);
} finally {
setTogglingTrigger(null);
}
}, [updateTrigger, loadTriggers]);
const handleDelete = useCallback(async (id: string) => {
setDeletingTrigger(id);
try {
await deleteTrigger(id);
await loadTriggers();
} catch (error) {
console.error('Failed to delete trigger:', error);
} finally {
setDeletingTrigger(null);
}
}, [deleteTrigger, loadTriggers]);
const handleRefresh = useCallback(async () => {
setRefreshing(true);
try {
await loadTriggers();
} finally {
setRefreshing(false);
}
}, [loadTriggers]);
const handleCreateSuccess = useCallback(() => {
loadTriggers();
}, [loadTriggers]);
if (isLoading && triggers.length === 0) {
return (
<div className="p-4 text-center text-gray-500 dark:text-gray-400">
...
</div>
);
}
const enabledCount = triggers.filter(t => t.enabled).length;
const totalCount = triggers.length;
return (
<>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
</h2>
<span className="text-sm text-gray-500 dark:text-gray-400">
{enabledCount}/{totalCount}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleRefresh}
disabled={refreshing}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-50 flex items-center gap-1"
>
<RefreshCw className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin' : ''}`} />
{refreshing ? '刷新中...' : '刷新'}
</button>
<button
onClick={() => setIsCreateModalOpen(true)}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
{triggers.length === 0 ? (
<div className="p-8 text-center bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="w-12 h-12 bg-amber-100 dark:bg-amber-900/30 rounded-full flex items-center justify-center mx-auto mb-3">
<Zap className="w-6 h-6 text-amber-600 dark:text-amber-400" />
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2"></p>
<p className="text-xs text-gray-400 dark:text-gray-500 mb-4 max-w-sm mx-auto">
API webhook
</p>
<button
onClick={() => setIsCreateModalOpen(true)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
) : (
<div className="grid gap-3">
{triggers.map((trigger) => (
<TriggerCard
key={trigger.id}
trigger={trigger}
onToggle={handleToggle}
onDelete={handleDelete}
isToggling={togglingTrigger === trigger.id}
isDeleting={deletingTrigger === trigger.id}
/>
))}
</div>
)}
</div>
<CreateTriggerModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onSuccess={handleCreateSuccess}
/>
</>
);
}
export default TriggersPanel;