- 设计 token 重映射为 DeepSeek 调色板(深浅双主题),正文改无衬线字体栈,全面去霓虹化 - 移除顶部 Header,控件迁入左栏(Logo/连接/通道/会话/tabs/底部操作区),左右栏支持拖拽调宽并持久化,折叠为图标 rail - 对话区对齐 DeepSeek 聊天风:hero/docked 状态机、736px 居中列、用户右对齐柔和气泡、助手无气泡平铺、composer 卡片与反差发送键;子智能体点击导航完整保留 - 全功能区同步换肤:侧栏列表、右栏面板、ConfigPage、弹窗、选择器等 - fix: 修复 virtual-core 3.17.x 陈旧行高导致消息行重叠(宽度变化/滚动停止后强制 resizeItem 重测) - fix: 修复 ConfigPage/Modal 引用不存在的 fadeIn/scaleIn keyframes 致入场动画失效
232 lines
8.8 KiB
TypeScript
232 lines
8.8 KiB
TypeScript
import { Clock, RefreshCw, ChevronRight, Check, X, Minus } from 'lucide-react';
|
|
import type { SchedulerJobSummary, SchedulerJobSessionLookup } from '../../types/protocol';
|
|
|
|
interface SchedulerJobListProps {
|
|
jobs: SchedulerJobSummary[];
|
|
onRefresh: () => void;
|
|
onViewJob: (lookup: SchedulerJobSessionLookup, jobId: string, description: string) => void;
|
|
sessionId: string | null;
|
|
}
|
|
|
|
function kindLabel(kind: string): string {
|
|
const map: Record<string, string> = {
|
|
internal_event: '内部事件',
|
|
outbound_message: '外发消息',
|
|
agent_task: '智能体',
|
|
silent_agent_task: '静默智能体',
|
|
};
|
|
return map[kind] ?? kind;
|
|
}
|
|
|
|
function stateBadge(state: string): { label: string; color: string; pulse: boolean } {
|
|
switch (state) {
|
|
case 'running':
|
|
return {
|
|
label: '执行中',
|
|
color: 'bg-[var(--accent-amber)]/15 text-[var(--accent-amber)] border-[var(--accent-amber)]/30',
|
|
pulse: true,
|
|
};
|
|
case 'scheduled':
|
|
return {
|
|
label: '已调度',
|
|
color: 'bg-[var(--accent-amber)]/10 text-[var(--accent-amber)] border-[var(--accent-amber)]/20',
|
|
pulse: false,
|
|
};
|
|
case 'paused':
|
|
return {
|
|
label: '已暂停',
|
|
color: 'bg-[var(--overlay-dim)] text-[var(--text-muted)] border-[var(--border-color)]',
|
|
pulse: false,
|
|
};
|
|
case 'completed':
|
|
return {
|
|
label: '已完成',
|
|
color: 'bg-[var(--accent-green)]/15 text-[var(--accent-green)] border-[var(--accent-green)]/30',
|
|
pulse: false,
|
|
};
|
|
default:
|
|
return {
|
|
label: state,
|
|
color: 'bg-[var(--overlay-dim)] text-[var(--text-muted)] border-[var(--border-color)]',
|
|
pulse: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
function formatTime(tsMillis: number | undefined): string {
|
|
if (tsMillis == null) return '--';
|
|
const d = new Date(tsMillis);
|
|
return d.toLocaleString('zh-CN', {
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
}
|
|
|
|
function scheduleDescription(schedule: unknown): string {
|
|
if (!schedule || typeof schedule !== 'object') return '--';
|
|
const s = schedule as Record<string, unknown>;
|
|
switch (s.type) {
|
|
case 'cron':
|
|
return `Cron: ${s.expression}`;
|
|
case 'interval':
|
|
return `每 ${s.seconds} 秒`;
|
|
case 'delay':
|
|
return `延迟 ${s.seconds} 秒`;
|
|
case 'at':
|
|
return `定时: ${s.timestamp}`;
|
|
default:
|
|
return JSON.stringify(s);
|
|
}
|
|
}
|
|
|
|
function lastStatusIcon(lastStatus: string | undefined) {
|
|
switch (lastStatus) {
|
|
case 'ok':
|
|
return <Check className="h-3 w-3 text-[var(--accent-green)]" />;
|
|
case 'error':
|
|
return <X className="h-3 w-3 text-[rgb(242,90,90)]" />;
|
|
default:
|
|
return <Minus className="h-3 w-3 text-[var(--text-muted)]" />;
|
|
}
|
|
}
|
|
|
|
export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: SchedulerJobListProps) {
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between border-b border-[var(--border-color)] px-3 py-2.5">
|
|
<h2 className="font-semibold text-[var(--text-primary)] flex items-center gap-2 text-sm">
|
|
<Clock className="h-4 w-4 text-[var(--accent-cyan)]" />
|
|
定时任务
|
|
{jobs.length > 0 && (
|
|
<span className="text-xs text-[var(--text-muted)]">({jobs.length})</span>
|
|
)}
|
|
</h2>
|
|
<button
|
|
onClick={onRefresh}
|
|
className="flex items-center gap-1 rounded-lg px-2 py-1 text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-subtle)] transition-all"
|
|
>
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
刷新
|
|
</button>
|
|
</div>
|
|
|
|
{/* Job List */}
|
|
<div className="flex-1 overflow-y-auto p-2">
|
|
{!sessionId ? (
|
|
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
|
|
<Clock className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p>等待连接...</p>
|
|
</div>
|
|
) : jobs.length === 0 ? (
|
|
<div className="p-4 text-center text-sm text-[var(--text-muted)]">
|
|
<Clock className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p>暂无定时任务</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1.5">
|
|
{jobs.map((job) => {
|
|
const badge = stateBadge(job.state);
|
|
const hasLookup = !!job.session_lookup;
|
|
return (
|
|
<button
|
|
key={job.id}
|
|
onClick={() => {
|
|
if (hasLookup && job.session_lookup) {
|
|
onViewJob(job.session_lookup, job.id, job.id);
|
|
}
|
|
}}
|
|
disabled={!hasLookup}
|
|
className={`w-full rounded-lg px-3 py-2.5 text-left transition-all border ${
|
|
hasLookup
|
|
? 'hover:bg-[var(--overlay-hover)] border-transparent hover:border-[var(--border-color)] cursor-pointer'
|
|
: 'border-transparent cursor-default opacity-70'
|
|
}`}
|
|
>
|
|
{/* Row 1: ID + kind + enabled */}
|
|
<div className="flex items-center justify-between mb-1">
|
|
<div className="flex items-center gap-1.5 min-w-0 flex-1">
|
|
<span className="text-xs text-[var(--text-muted)] font-mono truncate max-w-[120px]">
|
|
{job.id}
|
|
</span>
|
|
<span className="text-xs px-1.5 py-0.5 rounded bg-[var(--overlay-subtle)] text-[var(--text-secondary)] shrink-0">
|
|
{kindLabel(job.kind)}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5 shrink-0">
|
|
<span
|
|
className={`inline-block h-1.5 w-1.5 rounded-full ${job.enabled ? 'bg-[var(--accent-green)]' : 'bg-[var(--text-muted)]'}`}
|
|
/>
|
|
{hasLookup && (
|
|
<ChevronRight className="h-3.5 w-3.5 text-[var(--text-muted)]" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Row 2: State badge + last status */}
|
|
<div className="flex items-center gap-2 mb-1.5">
|
|
<span
|
|
className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded border ${badge.color}`}
|
|
>
|
|
{badge.pulse && (
|
|
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[var(--accent-amber)] animate-pulse" />
|
|
)}
|
|
{badge.label}
|
|
</span>
|
|
<span className="inline-flex items-center gap-0.5 text-xs text-[var(--text-muted)]">
|
|
{lastStatusIcon(job.last_status)}
|
|
<span
|
|
className={
|
|
job.last_status === 'error'
|
|
? 'text-[rgb(242,90,90)]'
|
|
: job.last_status === 'ok'
|
|
? 'text-[var(--accent-green)]'
|
|
: 'text-[var(--text-muted)]'
|
|
}
|
|
>
|
|
{job.last_status === 'ok'
|
|
? '正常'
|
|
: job.last_status === 'error'
|
|
? '异常'
|
|
: job.last_status === 'skipped'
|
|
? '跳过'
|
|
: '--'}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
|
|
{/* Row 3: Error message (if any) */}
|
|
{job.last_error && (
|
|
<div className="mb-1.5 text-xs text-[rgb(242,90,90)] truncate bg-[rgb(242,90,90)]/10 rounded px-1.5 py-0.5">
|
|
{job.last_error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Row 4: Run count + schedule */}
|
|
<div className="flex items-center justify-between text-xs text-[var(--text-muted)] mb-1">
|
|
<span>
|
|
执行: {job.run_count}
|
|
{job.max_runs ? `/${job.max_runs}` : ''}
|
|
</span>
|
|
<span className="truncate max-w-[120px] text-[var(--text-muted)]">
|
|
{scheduleDescription(job.schedule)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Row 5: Times */}
|
|
<div className="flex items-center justify-between text-xs text-[var(--text-muted)]">
|
|
<span>上次: {formatTime(job.last_fired_at)}</span>
|
|
<span>下次: {formatTime(job.next_fire_at)}</span>
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|