代码修改
This commit is contained in:
1 parent
ef1481be63
commit
a4b7ab5e9d
168 files changed
+76575
No files matched your search
@@ -0,0 +1,594 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Bot, Edit, Eye, Plus, Search, Sparkles, Trash2, X } from 'lucide-react';
|
||||
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
||||
interface AgentItem {
|
||||
key: string;
|
||||
type: 'builtin' | 'custom' | string;
|
||||
deletable: boolean;
|
||||
name: string;
|
||||
description: string;
|
||||
default_prompt: string;
|
||||
schema_hint: string;
|
||||
bound_skills: string[];
|
||||
order: number;
|
||||
}
|
||||
|
||||
interface SkillItem {
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface AgentCardListProps {
|
||||
onEdit?: (key: string) => void;
|
||||
}
|
||||
|
||||
type TypeFilter = 'all' | 'builtin' | 'custom';
|
||||
|
||||
interface CreateAgentForm {
|
||||
name: string;
|
||||
description: string;
|
||||
bound_skills: string[];
|
||||
}
|
||||
|
||||
const INITIAL_FORM: CreateAgentForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
bound_skills: [],
|
||||
};
|
||||
|
||||
const backUrl = 'https://ch1.b.uat.4.cn/zncmback';
|
||||
export default function AgentCardList({ onEdit }: AgentCardListProps) {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
const [agents, setAgents] = useState<AgentItem[]>([]);
|
||||
const [skills, setSkills] = useState<SkillItem[]>([]);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<TypeFilter>('all');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [createForm, setCreateForm] = useState<CreateAgentForm>(INITIAL_FORM);
|
||||
const [skillKeyword, setSkillKeyword] = useState('');
|
||||
|
||||
const showNotice = (message: string) => {
|
||||
setNotice(message);
|
||||
window.setTimeout(() => {
|
||||
setNotice((current) => (current === message ? null : current));
|
||||
}, 2400);
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [agentRes, skillRes] = await Promise.all([fetch(`${backUrl}/api/agents`), fetch(`${backUrl}/api/skills`)]);
|
||||
if (!agentRes.ok) {
|
||||
throw new Error(`加载 MCP 失败:${agentRes.status}`);
|
||||
}
|
||||
|
||||
const agentData = (await agentRes.json()) as AgentItem[];
|
||||
const skillData = skillRes.ok ? ((await skillRes.json()) as SkillItem[]) : [];
|
||||
|
||||
setAgents(agentData.sort((a, b) => a.order - b.order));
|
||||
setSkills(skillData);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
}, []);
|
||||
|
||||
const filteredAgents = useMemo(() => {
|
||||
const kw = keyword.trim().toLowerCase();
|
||||
return agents.filter((agent) => {
|
||||
const matchesKeyword =
|
||||
!kw ||
|
||||
agent.name.toLowerCase().includes(kw) ||
|
||||
agent.key.toLowerCase().includes(kw) ||
|
||||
agent.description.toLowerCase().includes(kw);
|
||||
|
||||
const matchesType = typeFilter === 'all' || agent.type === typeFilter;
|
||||
return matchesKeyword && matchesType;
|
||||
});
|
||||
}, [agents, keyword, typeFilter]);
|
||||
|
||||
const filteredSkills = useMemo(() => {
|
||||
const kw = skillKeyword.trim().toLowerCase();
|
||||
if (!kw) {
|
||||
return skills;
|
||||
}
|
||||
return skills.filter((skill) =>
|
||||
`${skill.name} ${skill.key} ${skill.description || ''}`.toLowerCase().includes(kw),
|
||||
);
|
||||
}, [skillKeyword, skills]);
|
||||
|
||||
const resetCreateForm = () => {
|
||||
setCreateForm(INITIAL_FORM);
|
||||
setSkillKeyword('');
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
resetCreateForm();
|
||||
setCreateOpen(true);
|
||||
};
|
||||
|
||||
const closeCreateModal = () => {
|
||||
setCreateOpen(false);
|
||||
resetCreateForm();
|
||||
};
|
||||
|
||||
const toggleSkill = (skillKey: string) => {
|
||||
setCreateForm((prev) => ({
|
||||
...prev,
|
||||
bound_skills: prev.bound_skills.includes(skillKey)
|
||||
? prev.bound_skills.filter((item) => item !== skillKey)
|
||||
: [...prev.bound_skills, skillKey],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
const name = createForm.name.trim();
|
||||
const description = createForm.description.trim();
|
||||
if (!name) {
|
||||
setError('请先输入 MCP 名称');
|
||||
return;
|
||||
}
|
||||
|
||||
setCreating(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`${backUrl}/api/agents`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description,
|
||||
bound_skills: createForm.bound_skills,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let detail = `新增失败:${res.status}`;
|
||||
try {
|
||||
const body = (await res.json()) as { detail?: string };
|
||||
if (body.detail) {
|
||||
detail = body.detail;
|
||||
}
|
||||
} catch {
|
||||
// ignore json parse errors
|
||||
}
|
||||
throw new Error(detail);
|
||||
}
|
||||
|
||||
await res.json();
|
||||
await loadData();
|
||||
closeCreateModal();
|
||||
showNotice('MCP 已新增');
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : '新增失败');
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (agent: AgentItem) => {
|
||||
if (!agent.deletable) return;
|
||||
const ok = window.confirm(`确认删除 MCP “${agent.name}”吗?`);
|
||||
if (!ok) return;
|
||||
|
||||
const res = await fetch(`${backUrl}/api/agents/${agent.key}`, { method: 'DELETE' });
|
||||
if (!res.ok) {
|
||||
setError(`删除失败:${res.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await loadData();
|
||||
showNotice('MCP 已删除');
|
||||
};
|
||||
|
||||
const selectedSkillCount = createForm.bound_skills.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 className={`mb-2 text-2xl font-bold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
MCP 智能体配置
|
||||
</h1>
|
||||
<p className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
管理内置与用户新增的 MCP,并支持绑定技能
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCreateModal}
|
||||
className={`inline-flex h-10 items-center justify-center gap-2 rounded-xl px-4 text-sm font-medium transition ${
|
||||
isDark ? 'bg-blue-600 text-white hover:bg-blue-500' : 'bg-red-600 text-white hover:bg-red-500'
|
||||
}`}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
新增 MCP
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="relative max-w-xl flex-1">
|
||||
<Search
|
||||
className={`absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 ${
|
||||
isDark ? 'text-slate-500' : 'text-red-400'
|
||||
}`}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder="搜索 MCP 名称、Key 或描述"
|
||||
className={`w-full rounded-xl border py-2.5 pl-10 pr-4 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-red-500/20 ${
|
||||
isDark
|
||||
? 'border-blue-800/30 bg-slate-800/50 text-white placeholder-slate-500'
|
||||
: 'border-red-200 bg-white text-gray-900 placeholder-red-300'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{[
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'builtin', label: '内置 MCP' },
|
||||
{ key: 'custom', label: '用户新增' },
|
||||
].map((item) => {
|
||||
const active = typeFilter === item.key;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => setTypeFilter(item.key as TypeFilter)}
|
||||
className={`rounded-full px-3 py-1.5 text-sm transition ${
|
||||
active
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-600 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-800 text-slate-300 hover:bg-slate-700'
|
||||
: 'bg-white text-gray-600 ring-1 ring-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? <div className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>加载中...</div> : null}
|
||||
{error ? <div className="text-sm text-red-500">{error}</div> : null}
|
||||
{notice ? <div className="text-sm text-emerald-500">{notice}</div> : null}
|
||||
|
||||
{!loading && !error ? (
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{filteredAgents.map((agent) => (
|
||||
<div
|
||||
key={agent.key}
|
||||
className={`group rounded-xl border p-5 transition-all duration-300 ${
|
||||
isDark
|
||||
? 'border-blue-800/30 bg-slate-800/30 backdrop-blur-xl hover:border-blue-500/50'
|
||||
: 'border-red-100 bg-white hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-4 flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-xl ${
|
||||
isDark ? 'bg-blue-500/20' : 'bg-red-100'
|
||||
}`}
|
||||
>
|
||||
<Bot className={`h-6 w-6 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{agent.name}
|
||||
</h3>
|
||||
<p className={`text-xs font-mono ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
{agent.key}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-1 text-xs font-medium ${
|
||||
agent.type === 'builtin'
|
||||
? isDark
|
||||
? 'bg-cyan-500/15 text-cyan-200'
|
||||
: 'bg-cyan-50 text-cyan-700'
|
||||
: isDark
|
||||
? 'bg-amber-500/15 text-amber-200'
|
||||
: 'bg-amber-50 text-amber-700'
|
||||
}`}
|
||||
>
|
||||
{agent.type === 'builtin' ? '内置' : '自定义'}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-2.5 py-1 text-xs font-medium ${
|
||||
isDark ? 'bg-slate-700/60 text-slate-300' : 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
#{agent.order + 1}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className={`mb-4 min-h-[40px] text-sm ${isDark ? 'text-slate-400' : 'text-gray-600'}`}>
|
||||
{agent.description || '暂无描述'}
|
||||
</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className={`mb-2 flex items-center gap-1 text-xs ${isDark ? 'text-slate-500' : 'text-gray-500'}`}>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
已绑定技能
|
||||
</div>
|
||||
<div className="flex min-h-[28px] flex-wrap gap-1.5">
|
||||
{agent.bound_skills.length === 0 ? (
|
||||
<span className={`text-xs ${isDark ? 'text-slate-600' : 'text-gray-400'}`}>未绑定</span>
|
||||
) : (
|
||||
agent.bound_skills.map((skillKey) => {
|
||||
const skill = skills.find((item) => item.key === skillKey);
|
||||
return (
|
||||
<span
|
||||
key={skillKey}
|
||||
className={`rounded-full border px-2 py-0.5 text-xs ${
|
||||
isDark
|
||||
? 'border-emerald-500/30 bg-emerald-500/15 text-emerald-300'
|
||||
: 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
||||
}`}
|
||||
>
|
||||
{skill?.name || skillKey}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex items-center justify-end gap-2 border-t pt-3 ${
|
||||
isDark ? 'border-blue-800/30' : 'border-red-100'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEdit?.(agent.key)}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-colors ${
|
||||
isDark
|
||||
? 'bg-slate-700/60 text-slate-200 hover:bg-slate-700'
|
||||
: 'border border-red-200 bg-white text-red-600 hover:bg-red-50'
|
||||
}`}
|
||||
>
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
详情
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEdit?.(agent.key)}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-colors ${
|
||||
isDark ? 'bg-blue-600 text-white hover:bg-blue-500' : 'bg-red-600 text-white hover:bg-red-500'
|
||||
}`}
|
||||
>
|
||||
<Edit className="h-3.5 w-3.5" />
|
||||
编辑
|
||||
</button>
|
||||
{agent.deletable ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleDelete(agent)}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-colors ${
|
||||
isDark
|
||||
? 'bg-rose-500/15 text-rose-200 hover:bg-rose-500/25'
|
||||
: 'bg-rose-50 text-rose-600 hover:bg-rose-100'
|
||||
}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
删除
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!loading && !error && filteredAgents.length === 0 ? (
|
||||
<div className={`py-12 text-center text-sm ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
没有匹配的 MCP
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{createOpen ? (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4 backdrop-blur-sm"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeCreateModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`max-h-[85vh] w-full max-w-2xl overflow-hidden rounded-3xl border shadow-2xl ${
|
||||
isDark ? 'border-slate-700 bg-slate-900 text-slate-100' : 'border-red-100 bg-white text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b px-6 py-5 dark:border-slate-700">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">新增 MCP</h2>
|
||||
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
输入 MCP 名称、详情并选择要绑定的技能
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeCreateModal}
|
||||
className={`flex h-9 w-9 items-center justify-center rounded-xl ${
|
||||
isDark ? 'hover:bg-slate-800' : 'hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 overflow-y-auto px-6 py-5">
|
||||
<div>
|
||||
<label className={`mb-2 block text-sm font-medium ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
|
||||
MCP 名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={createForm.name}
|
||||
onChange={(e) => setCreateForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||
placeholder="例如:游戏分析助手"
|
||||
className={`w-full rounded-xl border px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-red-500/20 ${
|
||||
isDark
|
||||
? 'border-slate-700 bg-slate-950 text-white placeholder:text-slate-500'
|
||||
: 'border-gray-200 bg-white text-gray-900 placeholder:text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={`mb-2 block text-sm font-medium ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
|
||||
详情
|
||||
</label>
|
||||
<textarea
|
||||
value={createForm.description}
|
||||
onChange={(e) => setCreateForm((prev) => ({ ...prev, description: e.target.value }))}
|
||||
placeholder="一句话说明这个 MCP 的职责"
|
||||
rows={4}
|
||||
className={`w-full rounded-xl border px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-red-500/20 ${
|
||||
isDark
|
||||
? 'border-slate-700 bg-slate-950 text-white placeholder:text-slate-500'
|
||||
: 'border-gray-200 bg-white text-gray-900 placeholder:text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<label className={`block text-sm font-medium ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
|
||||
绑定技能
|
||||
</label>
|
||||
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
已选 {selectedSkillCount} 项
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
value={skillKeyword}
|
||||
onChange={(e) => setSkillKeyword(e.target.value)}
|
||||
placeholder="搜索技能名称或描述"
|
||||
className={`w-full rounded-xl border px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-red-500/20 ${
|
||||
isDark
|
||||
? 'border-slate-700 bg-slate-950 text-white placeholder:text-slate-500'
|
||||
: 'border-gray-200 bg-white text-gray-900 placeholder:text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`max-h-72 space-y-2 overflow-y-auto rounded-2xl border p-3 ${
|
||||
isDark ? 'border-slate-700 bg-slate-950/60' : 'border-gray-200 bg-gray-50/70'
|
||||
}`}
|
||||
>
|
||||
{filteredSkills.length === 0 ? (
|
||||
<div className={`py-8 text-center text-sm ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
没有匹配的技能
|
||||
</div>
|
||||
) : (
|
||||
filteredSkills.map((skill) => {
|
||||
const checked = createForm.bound_skills.includes(skill.key);
|
||||
return (
|
||||
<button
|
||||
key={skill.key}
|
||||
type="button"
|
||||
onClick={() => toggleSkill(skill.key)}
|
||||
className={`flex w-full items-start gap-3 rounded-xl border px-3 py-3 text-left transition ${
|
||||
checked
|
||||
? isDark
|
||||
? 'border-emerald-500/50 bg-emerald-500/10'
|
||||
: 'border-emerald-200 bg-emerald-50'
|
||||
: isDark
|
||||
? 'border-slate-700 bg-slate-900/50 hover:bg-slate-800/70'
|
||||
: 'border-white bg-white hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`mt-1 h-4 w-4 shrink-0 rounded border ${
|
||||
checked
|
||||
? 'border-emerald-500 bg-emerald-500'
|
||||
: isDark
|
||||
? 'border-slate-600'
|
||||
: 'border-gray-300'
|
||||
}`}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className={`text-sm font-medium ${isDark ? 'text-slate-100' : 'text-gray-900'}`}>
|
||||
{skill.name}
|
||||
<span className={`ml-2 font-mono text-[11px] ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
{skill.key}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`mt-1 text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{skill.description || '暂无描述'}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex items-center justify-end gap-3 border-t px-6 py-4 ${
|
||||
isDark ? 'border-slate-700' : 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeCreateModal}
|
||||
className={`rounded-xl px-4 py-2 text-sm ${
|
||||
isDark ? 'text-slate-300 hover:bg-slate-800' : 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={creating}
|
||||
className={`rounded-xl px-4 py-2 text-sm font-medium text-white ${
|
||||
isDark ? 'bg-blue-600 hover:bg-blue-500' : 'bg-red-600 hover:bg-red-500'
|
||||
} disabled:cursor-not-allowed disabled:opacity-60`}
|
||||
>
|
||||
{creating ? '创建中...' : '确认新增'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user