feat: 完善工作台展示场景 - 新增 Modal/Toast 组件、EmptyState 使用、确认弹窗、筛选分页

- 新增 Modal 确认弹窗组件和 Toast 消息提示组件
- 在 SkillsPage、LogsPage、TasksPage、ProjectsPage 使用 EmptyState
- 为删除任务、取消订阅、移除成员、技能订阅添加确认弹窗
- 丰富聊天场景:代码展示、表格数据、多轮对话、错误提示
- 优化 ChatPage 布局,修复对话区域滚动问题
- 为 ProjectsPage 添加筛选卡片和分页组件
- 添加表单校验错误状态展示
- 同步 specs 到主目录
This commit is contained in:
2026-03-20 11:44:25 +08:00
parent 9f407c3aea
commit 181cf09ad2
17 changed files with 1147 additions and 229 deletions

View File

@@ -1,4 +1,41 @@
import { useState } from 'react';
import Toast from '../../components/common/Toast.jsx';
function AccountPage() {
const [profileToast, setProfileToast] = useState(null);
const [passwordErrors, setPasswordErrors] = useState({});
const [passwordForm, setPasswordForm] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const handleProfileSave = () => {
setProfileToast({ type: 'success', message: '保存成功' });
setTimeout(() => setProfileToast(null), 3000);
};
const handlePasswordChange = (field, value) => {
setPasswordForm(prev => ({ ...prev, [field]: value }));
setPasswordErrors(prev => ({ ...prev, [field]: '' }));
};
const handlePasswordSubmit = () => {
const errors = {};
if (!passwordForm.currentPassword) {
errors.currentPassword = '请输入当前密码';
}
if (!passwordForm.newPassword) {
errors.newPassword = '请输入新密码';
}
if (!passwordForm.confirmPassword) {
errors.confirmPassword = '请再次输入新密码';
} else if (passwordForm.newPassword !== passwordForm.confirmPassword) {
errors.confirmPassword = '两次输入的密码不一致';
}
setPasswordErrors(errors);
};
return (
<>
<div className="card">
@@ -55,7 +92,7 @@ function AccountPage() {
<label className="form-label">所属部门</label>
<input type="text" className="form-control" defaultValue="AI 产品部" readOnly style={{ background: '#F8FAFC' }} />
</div>
<button className="btn btn-primary">保存修改</button>
<button className="btn btn-primary" onClick={handleProfileSave}>保存修改</button>
</div>
</div>
<div className="card">
@@ -65,19 +102,52 @@ function AccountPage() {
<div className="card-body">
<div className="form-group">
<label className="form-label">当前密码</label>
<input type="password" className="form-control" placeholder="请输入当前密码" />
<input
type="password"
className={`form-control ${passwordErrors.currentPassword ? 'is-invalid' : ''}`}
placeholder="请输入当前密码"
value={passwordForm.currentPassword}
onChange={e => handlePasswordChange('currentPassword', e.target.value)}
/>
{passwordErrors.currentPassword && (
<div className="form-error">{passwordErrors.currentPassword}</div>
)}
</div>
<div className="form-group">
<label className="form-label">新密码</label>
<input type="password" className="form-control" placeholder="请输入新密码" />
<input
type="password"
className={`form-control ${passwordErrors.newPassword ? 'is-invalid' : ''}`}
placeholder="请输入新密码"
value={passwordForm.newPassword}
onChange={e => handlePasswordChange('newPassword', e.target.value)}
/>
{passwordErrors.newPassword && (
<div className="form-error">{passwordErrors.newPassword}</div>
)}
</div>
<div className="form-group">
<label className="form-label">确认新密码</label>
<input type="password" className="form-control" placeholder="请再次输入新密码" />
<input
type="password"
className={`form-control ${passwordErrors.confirmPassword ? 'is-invalid' : ''}`}
placeholder="请再次输入新密码"
value={passwordForm.confirmPassword}
onChange={e => handlePasswordChange('confirmPassword', e.target.value)}
/>
{passwordErrors.confirmPassword && (
<div className="form-error">{passwordErrors.confirmPassword}</div>
)}
</div>
<button className="btn btn-primary">更新密码</button>
<button className="btn btn-primary" onClick={handlePasswordSubmit}>更新密码</button>
</div>
</div>
<Toast
visible={!!profileToast}
type={profileToast?.type}
message={profileToast?.message}
onClose={() => setProfileToast(null)}
/>
</>
);
}

View File

@@ -30,12 +30,12 @@ function ChatPage({ scene }) {
}, [scene, html]); // 依赖场景和html内容
return (
<div className="chat-layout" style={{ height: '100%' }}>
<div className="chat-layout">
<div className="chat-content">
<div className="chat-messages" style={{ padding: '16px 24px 8px' }}>
<div className="chat-messages">
<div ref={chatMessagesRef} dangerouslySetInnerHTML={{ __html: html }} />
</div>
<div className="chat-input-wrapper" style={{ padding: '12px 24px 20px' }}>
<div className="chat-input-wrapper">
<div className="chat-input-container">
<div className="chat-input-box">
<div className="chat-input-main">

View File

@@ -1,6 +1,40 @@
import { useState } from 'react';
import { FiInbox } from 'react-icons/fi';
import { logs } from '../../data/logs.js';
import EmptyState from '../../components/common/EmptyState.jsx';
function LogsPage() {
const [filters, setFilters] = useState({
keyword: '',
user: '',
type: '',
status: '',
});
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const handleReset = () => {
setFilters({ keyword: '', user: '', type: '', status: '' });
};
const filteredLogs = logs.filter(log => {
if (filters.keyword && !log.action.includes(filters.keyword) && !log.detail.includes(filters.keyword)) {
return false;
}
if (filters.user && log.user !== filters.user) {
return false;
}
if (filters.type && log.type !== filters.type) {
return false;
}
if (filters.status && log.status !== filters.status) {
return false;
}
return true;
});
return (
<>
<div className="card">
@@ -8,12 +42,22 @@ function LogsPage() {
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索操作、详情..." />
<input
type="text"
className="form-control"
placeholder="搜索操作、详情..."
value={filters.keyword}
onChange={e => handleFilterChange('keyword', e.target.value)}
/>
</div>
<div className="search-item">
<label>用户</label>
<select className="form-control">
<option>全部用户</option>
<select
className="form-control"
value={filters.user}
onChange={e => handleFilterChange('user', e.target.value)}
>
<option value="">全部用户</option>
<option>张三</option>
<option>李四</option>
<option>王五</option>
@@ -21,8 +65,12 @@ function LogsPage() {
</div>
<div className="search-item">
<label>类型</label>
<select className="form-control">
<option>全部类型</option>
<select
className="form-control"
value={filters.type}
onChange={e => handleFilterChange('type', e.target.value)}
>
<option value="">全部类型</option>
<option>登录</option>
<option>实例操作</option>
<option>技能</option>
@@ -32,8 +80,12 @@ function LogsPage() {
</div>
<div className="search-item">
<label>状态</label>
<select className="form-control">
<option>全部</option>
<select
className="form-control"
value={filters.status}
onChange={e => handleFilterChange('status', e.target.value)}
>
<option value="">全部</option>
<option>成功</option>
<option>失败</option>
<option>警告</option>
@@ -42,7 +94,7 @@ function LogsPage() {
</div>
<div className="search-actions">
<button className="btn btn-primary">查询</button>
<button className="btn">重置</button>
<button className="btn" onClick={handleReset}>重置</button>
</div>
</div>
</div>
@@ -52,39 +104,49 @@ function LogsPage() {
<button className="btn btn-primary btn-sm">导出日志</button>
</div>
<div className="card-body">
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>类型</th>
<th>操作</th>
<th>状态</th>
<th>详情</th>
</tr>
</thead>
<tbody>
{logs.map((log, index) => (
<tr key={index}>
<td>{log.time}</td>
<td>{log.user}</td>
<td>{log.type}</td>
<td>{log.action}</td>
<td><span className={`status ${log.status === '成功' ? 'status-running' : log.status === '失败' ? 'status-error' : 'status-warning'}`}>{log.status}</span></td>
<td>{log.detail}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item">2</div>
<div className="pagination-item">3</div>
<div className="pagination-item"></div>
</div>
{filteredLogs.length > 0 ? (
<>
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>类型</th>
<th>操作</th>
<th>状态</th>
<th>详情</th>
</tr>
</thead>
<tbody>
{filteredLogs.map((log, index) => (
<tr key={index}>
<td>{log.time}</td>
<td>{log.user}</td>
<td>{log.type}</td>
<td>{log.action}</td>
<td><span className={`status ${log.status === '成功' ? 'status-running' : log.status === '失败' ? 'status-error' : 'status-warning'}`}>{log.status}</span></td>
<td>{log.detail}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item">2</div>
<div className="pagination-item">3</div>
<div className="pagination-item"></div>
</div>
</>
) : (
<EmptyState
icon={<FiInbox size={48} />}
message="暂无匹配日志"
description="当前筛选条件下没有日志记录"
/>
)}
</div>
</div>
</>

View File

@@ -1,44 +1,148 @@
import { useState } from 'react';
import { FiUsers, FiSearch } from 'react-icons/fi';
import { projectMembers } from '../../data/members.js';
import EmptyState from '../../components/common/EmptyState.jsx';
import Modal from '../../components/common/Modal.jsx';
function ProjectsPage({ onAddMember }) {
const [members, setMembers] = useState(projectMembers);
const [removeTarget, setRemoveTarget] = useState(null);
const [filters, setFilters] = useState({
keyword: '',
role: '',
});
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
};
const handleReset = () => {
setFilters({ keyword: '', role: '' });
};
const filteredMembers = members.filter(member => {
if (filters.keyword && !member.name.includes(filters.keyword)) {
return false;
}
if (filters.role && member.role !== filters.role) {
return false;
}
return true;
});
const handleRemoveClick = (member) => {
setRemoveTarget(member);
};
const confirmRemove = () => {
if (removeTarget) {
setMembers(prev => prev.filter(m => m.id !== removeTarget.id));
setRemoveTarget(null);
}
};
const cancelRemove = () => {
setRemoveTarget(null);
};
return (
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">项目管理</div>
<button className="btn btn-primary btn-sm" onClick={onAddMember}>增加成员</button>
</div>
<div className="card-body">
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>成员</th>
<th style={{ width: '100px' }}>角色</th>
<th style={{ width: '80px' }}>操作</th>
</tr>
</thead>
<tbody>
{projectMembers.map(member => (
<tr key={member.id}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div className="user-avatar" style={{ width: '32px', height: '32px', fontSize: '13px' }}>
{member.name.charAt(0)}
</div>
{member.name}
</div>
</td>
<td><span className={`status ${member.role === '管理员' ? 'role-admin' : 'role-member'}`}>{member.role}</span></td>
<td style={{ width: '80px' }}>
<button className="text-btn text-btn-primary">配置</button>
</td>
</tr>
))}
</tbody>
</table>
<>
<div className="card">
<div className="card-body">
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input
type="text"
className="form-control"
placeholder="搜索成员姓名..."
value={filters.keyword}
onChange={e => handleFilterChange('keyword', e.target.value)}
/>
</div>
<div className="search-item">
<label>角色</label>
<select
className="form-control"
value={filters.role}
onChange={e => handleFilterChange('role', e.target.value)}
>
<option value="">全部角色</option>
<option>管理员</option>
<option>成员</option>
</select>
</div>
</div>
<div className="search-actions">
<button className="btn btn-primary"><FiSearch /> 查询</button>
<button className="btn" onClick={handleReset}>重置</button>
</div>
</div>
</div>
</div>
<div className="card">
<div className="card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="card-title">成员列表</div>
<button className="btn btn-primary btn-sm" onClick={onAddMember}>增加成员</button>
</div>
<div className="card-body">
{filteredMembers.length > 0 ? (
<>
<div className="table-wrapper">
<table className="table">
<thead>
<tr>
<th>成员</th>
<th style={{ width: '100px' }}>角色</th>
<th style={{ width: '120px' }}>操作</th>
</tr>
</thead>
<tbody>
{filteredMembers.map(member => (
<tr key={member.id}>
<td>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<div className="user-avatar" style={{ width: '32px', height: '32px', fontSize: '13px' }}>
{member.name.charAt(0)}
</div>
{member.name}
</div>
</td>
<td><span className={`status ${member.role === '管理员' ? 'role-admin' : 'role-member'}`}>{member.role}</span></td>
<td style={{ width: '120px' }}>
<button className="text-btn text-btn-primary">配置</button>
<button className="text-btn text-btn-danger" style={{ marginLeft: '8px' }} onClick={() => handleRemoveClick(member)}>移除</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="pagination">
<div className="pagination-item"></div>
<div className="pagination-item active">1</div>
<div className="pagination-item">2</div>
<div className="pagination-item"></div>
</div>
</>
) : (
<EmptyState
icon={<FiUsers size={48} />}
message="暂无匹配成员"
description={filters.keyword || filters.role ? '当前筛选条件下没有成员' : '还没有添加任何项目成员'}
/>
)}
</div>
</div>
<Modal
visible={!!removeTarget}
title="确认移除"
onConfirm={confirmRemove}
onCancel={cancelRemove}
confirmText="移除"
>
确定要将成员"{removeTarget?.name}"移出项目吗
</Modal>
</>
);
}

View File

@@ -1,12 +1,34 @@
import { useState } from 'react';
import { FiChevronLeft, FiFile } from 'react-icons/fi';
import { skills, getSkillIcon, skillFiles, skillVersions } from '../../data/skills.js';
import Modal from '../../components/common/Modal.jsx';
function SkillDetailPage({ skillId, onBack }) {
const skill = skills.find(s => s.id === skillId);
const [subscribed, setSubscribed] = useState(skill?.subscribed || false);
const [showUnsubModal, setShowUnsubModal] = useState(false);
if (!skill) {
return <div>Skill not found</div>;
}
const handleSubscribeClick = () => {
if (subscribed) {
setShowUnsubModal(true);
} else {
setSubscribed(true);
}
};
const confirmUnsubscribe = () => {
setSubscribed(false);
setShowUnsubModal(false);
};
const cancelUnsubscribe = () => {
setShowUnsubModal(false);
};
return (
<>
<div className="skill-back-btn" onClick={onBack}>
@@ -30,8 +52,8 @@ function SkillDetailPage({ skillId, onBack }) {
</div>
</div>
<div style={{ flexShrink: 0 }}>
<button className={`btn ${skill.subscribed ? '' : 'btn-primary'}`}>
{skill.subscribed ? '取消订阅' : '立即订阅'}
<button className={`btn ${subscribed ? '' : 'btn-primary'}`} onClick={handleSubscribeClick}>
{subscribed ? '取消订阅' : '立即订阅'}
</button>
</div>
</div>
@@ -70,6 +92,15 @@ function SkillDetailPage({ skillId, onBack }) {
</div>
</div>
</div>
<Modal
visible={showUnsubModal}
title="确认取消订阅"
onConfirm={confirmUnsubscribe}
onCancel={cancelUnsubscribe}
confirmText="取消订阅"
>
确定要取消订阅"{skill.name}"取消后将无法使用该技能
</Modal>
</>
);
}

View File

@@ -1,8 +1,11 @@
import { useState } from 'react';
import { FiUser, FiStar, FiSearch } from 'react-icons/fi';
import { FaBoxOpen } from 'react-icons/fa';
import { skills, getSkillIcon } from '../../data/skills.js';
import EmptyState from '../../components/common/EmptyState.jsx';
import Modal from '../../components/common/Modal.jsx';
function SkillCard({ skill, onClick }) {
function SkillCard({ skill, onClick, onSubscribe }) {
return (
<div className="skill-card" onClick={onClick}>
<div className="skill-header">
@@ -27,7 +30,10 @@ function SkillCard({ skill, onClick }) {
<FiStar /> {skill.rating}
</span>
</div>
<button className={`btn ${skill.subscribed ? 'btn-primary' : ''} btn-sm`} onClick={e => e.stopPropagation()}>
<button
className={`btn ${skill.subscribed ? 'btn-primary' : ''} btn-sm`}
onClick={e => { e.stopPropagation(); onSubscribe(skill); }}
>
{skill.subscribed ? '已订阅' : '订阅'}
</button>
</div>
@@ -38,17 +44,45 @@ function SkillCard({ skill, onClick }) {
function SkillsPage({ onSkillClick }) {
const [filter, setFilter] = useState('all');
const [sort, setSort] = useState('subs');
const [searchQuery, setSearchQuery] = useState('');
const [skillsState, setSkillsState] = useState(skills);
const [modalTarget, setModalTarget] = useState(null);
const filteredSkills = filter === 'subscribed'
? skills.filter(s => s.subscribed)
: [...skills];
? skillsState.filter(s => s.subscribed)
: [...skillsState];
filteredSkills.sort((a, b) => {
const searchedSkills = searchQuery
? filteredSkills.filter(s =>
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.desc.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.tags.some(t => t.toLowerCase().includes(searchQuery.toLowerCase()))
)
: filteredSkills;
searchedSkills.sort((a, b) => {
if (sort === 'subs') return b.subs - a.subs;
if (sort === 'rating') return b.rating - a.rating;
return 0;
});
const handleSubscribeClick = (skill) => {
setModalTarget(skill);
};
const confirmSubscribe = () => {
if (modalTarget) {
setSkillsState(prev => prev.map(s =>
s.id === modalTarget.id ? { ...s, subscribed: !s.subscribed } : s
));
setModalTarget(null);
}
};
const cancelSubscribe = () => {
setModalTarget(null);
};
return (
<>
<div className="card">
@@ -56,7 +90,13 @@ function SkillsPage({ onSkillClick }) {
<div className="search-bar">
<div className="search-item" style={{ flex: 1, minWidth: '200px' }}>
<label>关键词</label>
<input type="text" className="form-control" placeholder="搜索技能..." />
<input
type="text"
className="form-control"
placeholder="搜索技能..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
</div>
<div className="search-item">
<label>分类</label>
@@ -85,15 +125,40 @@ function SkillsPage({ onSkillClick }) {
<div className="btn-group">
<button className={`btn ${filter === 'all' ? 'btn-primary' : ''}`} onClick={() => setFilter('all')}>全部技能</button>
<button className={`btn ${filter === 'subscribed' ? 'btn-primary' : ''}`} onClick={() => setFilter('subscribed')}>
已订阅 ({skills.filter(s => s.subscribed).length})
已订阅 ({skillsState.filter(s => s.subscribed).length})
</button>
</div>
</div>
<div className="skill-grid">
{filteredSkills.map(skill => (
<SkillCard key={skill.id} skill={skill} onClick={() => onSkillClick(skill.id)} />
))}
</div>
{searchedSkills.length > 0 ? (
<div className="skill-grid">
{searchedSkills.map(skill => (
<SkillCard
key={skill.id}
skill={skill}
onClick={() => onSkillClick(skill.id)}
onSubscribe={handleSubscribeClick}
/>
))}
</div>
) : (
<EmptyState
icon={<FaBoxOpen size={48} />}
message="暂无匹配技能"
description={searchQuery ? `未找到与"${searchQuery}"相关的技能` : '当前筛选条件下没有技能'}
/>
)}
<Modal
visible={!!modalTarget}
title={modalTarget?.subscribed ? '确认取消订阅' : '确认订阅'}
onConfirm={confirmSubscribe}
onCancel={cancelSubscribe}
confirmText={modalTarget?.subscribed ? '取消订阅' : '订阅'}
>
{modalTarget?.subscribed
? `确定要取消订阅"${modalTarget?.name}"吗?取消后将无法使用该技能。`
: `确定要订阅"${modalTarget?.name}"吗?`
}
</Modal>
</>
);
}

View File

@@ -1,8 +1,12 @@
import { useState } from 'react';
import { FiClock } from 'react-icons/fi';
import { scheduledTasks } from '../../data/tasks.js';
import EmptyState from '../../components/common/EmptyState.jsx';
import Modal from '../../components/common/Modal.jsx';
function TasksPage({ onViewDetail }) {
const [tasks, setTasks] = useState(scheduledTasks);
const [deleteTarget, setDeleteTarget] = useState(null);
const toggleTask = (taskId) => {
setTasks(prev => prev.map(task =>
@@ -10,50 +14,84 @@ function TasksPage({ onViewDetail }) {
));
};
const handleDeleteClick = (task) => {
setDeleteTarget(task);
};
const confirmDelete = () => {
if (deleteTarget) {
setTasks(prev => prev.filter(task => task.id !== deleteTarget.id));
setDeleteTarget(null);
}
};
const cancelDelete = () => {
setDeleteTarget(null);
};
return (
<div className="card">
<div className="card-header">
<div className="card-title">定时任务</div>
<>
<div className="card">
<div className="card-header">
<div className="card-title">定时任务</div>
</div>
<div className="card-body">
{tasks.length > 0 ? (
<table className="table">
<thead>
<tr>
<th>任务名称</th>
<th>频率</th>
<th>上次触发</th>
<th>下次触发</th>
<th>上次运行状态</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{tasks.map(task => (
<tr key={task.id}>
<td>{task.name}</td>
<td>{task.frequency}</td>
<td>{task.lastTriggered}</td>
<td>{task.nextTrigger}</td>
<td>
<span className={`status ${task.lastStatus === '成功' ? 'status-running' : task.lastStatus === '失败' ? 'status-error' : 'status-warning'}`}>
{task.lastStatus}
</span>
</td>
<td><span className={`status ${task.enabled ? 'status-running' : 'status-stopped'}`}>{task.enabled ? '启用' : '禁用'}</span></td>
<td>
<button className={`text-btn ${task.enabled ? 'text-btn-danger' : 'text-btn-primary'}`} onClick={() => toggleTask(task.id)}>
{task.enabled ? '禁用' : '启用'}
</button>
<button className="text-btn text-btn-primary" style={{ marginLeft: '8px' }} onClick={() => onViewDetail(task.id)}>详情</button>
<button className="text-btn text-btn-danger" style={{ marginLeft: '8px' }} onClick={() => handleDeleteClick(task)}>删除</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<EmptyState
icon={<FiClock size={48} />}
message="暂无定时任务"
description="还没有创建任何定时任务"
/>
)}
</div>
</div>
<div className="card-body">
<table className="table">
<thead>
<tr>
<th>任务名称</th>
<th>频率</th>
<th>上次触发</th>
<th>下次触发</th>
<th>上次运行状态</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{tasks.map(task => (
<tr key={task.id}>
<td>{task.name}</td>
<td>{task.frequency}</td>
<td>{task.lastTriggered}</td>
<td>{task.nextTrigger}</td>
<td>
<span className={`status ${task.lastStatus === '成功' ? 'status-running' : task.lastStatus === '失败' ? 'status-error' : 'status-warning'}`}>
{task.lastStatus}
</span>
</td>
<td><span className={`status ${task.enabled ? 'status-running' : 'status-stopped'}`}>{task.enabled ? '启用' : '禁用'}</span></td>
<td>
<button className={`text-btn ${task.enabled ? 'text-btn-danger' : 'text-btn-primary'}`} onClick={() => toggleTask(task.id)}>
{task.enabled ? '禁用' : '启用'}
</button>
<button className="text-btn text-btn-primary" style={{ marginLeft: '8px' }} onClick={() => onViewDetail(task.id)}>详情</button>
<button className="text-btn text-btn-danger" style={{ marginLeft: '8px' }}>删除</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<Modal
visible={!!deleteTarget}
title="确认删除"
onConfirm={confirmDelete}
onCancel={cancelDelete}
confirmText="删除"
>
确定要删除任务"{deleteTarget?.name}"此操作不可撤销
</Modal>
</>
);
}