@yancyyu/openhermit 1.6.38 → 1.6.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-renderer/assets/ProjectEditorOverlay-krO5vQxX.js +58 -0
- package/dist-renderer/assets/{TeamGraphOverlay-ZEDfZyHb.js → TeamGraphOverlay-DqhQzcTr.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-CIhniz70.js → _basePickBy-B7kSYPxr.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-cKAW4Q8I.js → _baseUniq-CnjxqwAk.js} +1 -1
- package/dist-renderer/assets/{arc-YmNsoDXW.js → arc-CLeZuINP.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DHEls2sX.js → architectureDiagram-VXUJARFQ-QKtqaqdY.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-Bpwf1Sbg.js → blockDiagram-VD42YOAC-BqdrzO_f.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B0IaQ4w5.js → c4Diagram-YG6GDRKO-gwPlCxDC.js} +1 -1
- package/dist-renderer/assets/channel-DpMHF50r.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-DLk-hcFc.js → chunk-4BX2VUAB-C6XLurL4.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-1XRmX_Zm.js → chunk-55IACEB6-Ds6quhEP.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-1waH1DAD.js → chunk-B4BG7PRW-5UlA1_e9.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-BqpZBtrN.js → chunk-DI55MBZ5-ywFrqIsY.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-Bly7vVym.js → chunk-FMBD7UC4-C7ifUA17.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-Ci2QWBAs.js → chunk-QN33PNHL-BxGCo80U.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-YCqFW7d-.js → chunk-QZHKN3VN-B2CuaZs6.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-B0xGXInl.js → chunk-TZMSLE5B-Ds1hInvp.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CBYCBVRl.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CBYCBVRl.js +1 -0
- package/dist-renderer/assets/clone-DcMF6Psb.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DxcFNQKT.js → cose-bilkent-S5V4N54A-Cz1GVtLp.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DPo_RfZY.js → dagre-6UL2VRFP-BrmR-P4h.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-U3hQsFe4.js → diagram-PSM6KHXK-DbNjC5Rg.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-OrwrAy0V.js → diagram-QEK2KX5R-qkRX5_Mq.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-CXATPWVw.js → diagram-S2PKOQOG-CyL5rCv2.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-B0e8AfMF.js → erDiagram-Q2GNP2WA-Dox3-bA5.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-CXfzA4jJ.js → flowDiagram-NV44I4VS-BtkaxlDL.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CMr08qVl.js → ganttDiagram-JELNMOA3-Dhy_d9GK.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vYFHpPmy.js → gitGraphDiagram-V2S2FVAM-B5XRhIQA.js} +1 -1
- package/dist-renderer/assets/{graph-DOe5j8dH.js → graph-CsoEwUhS.js} +1 -1
- package/dist-renderer/assets/{index-BySQS7AB.js → index-BWPWmJNo.js} +1 -1
- package/dist-renderer/assets/{index-V7dAKPqd.js → index-Bu2R-Se7.js} +587 -705
- package/dist-renderer/assets/index-CnWV3BhG.css +32 -0
- package/dist-renderer/assets/{index-CzWxVCRL.js → index-D-3KgskL.js} +1 -1
- package/dist-renderer/assets/{index-VJ-MM9xa.js → index-DGEBzLNT.js} +1 -1
- package/dist-renderer/assets/{index-B2Dy7M2G.js → index-NhHNs2Oo.js} +1 -1
- package/dist-renderer/assets/{index-C_okzZXP.js → index-h17WuEyf.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D_WubR0B.js → infoDiagram-HS3SLOUP-hMGmNojH.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-w9ca-1TI.js → journeyDiagram-XKPGCS4Q-DXV2rBDl.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-Jg9p6_pN.js → kanban-definition-3W4ZIXB7-Bf99WLRy.js} +1 -1
- package/dist-renderer/assets/{layout-B-z3y17c.js → layout-C3XWrpwo.js} +1 -1
- package/dist-renderer/assets/{linear-D-RTX5UW.js → linear-OEEcn8KN.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CDQmHOYP.js → mindmap-definition-VGOIOE7T-Dpi3S2x4.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-D_odsQL7.js → pieDiagram-ADFJNKIX-xTPPhtNx.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BRsmYWSA.js → quadrantDiagram-AYHSOK5B-euniyDlz.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-ChNE_BOV.js → requirementDiagram-UZGBJVZJ-D9Uiw4kF.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8FtpwKc.js → sankeyDiagram-TZEHDZUN-CySU4nED.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DmLCzNcc.js → sequenceDiagram-WL72ISMW-JVGpET6V.js} +1 -1
- package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-WJBm4bhu.js → stateDiagram-FKZM4ZOC-B2FY5qqi.js} +1 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DcoMiR8H.js +1 -0
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BXs_hOJs.js → timeline-definition-IT6M3QCI-DmycNUUe.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-o04MA0G9.js → treemap-GDKQZRPO-DPq4gZuB.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-Czj69XRd.js → xychartDiagram-PRI3JC2R-J6VVJzRq.js} +1 -1
- package/dist-renderer/index.html +20 -53
- package/package.json +25 -18
- package/src/main/ipc/extensions.ts +2 -1
- package/src/main/server.ts +873 -221
- package/src/main/services/extensions/ExtensionFacadeService.ts +2 -5
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
- package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
- package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
- package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
- package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
- package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
- package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +5 -6
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
- package/src/main/utils/teamProjectResolution.ts +15 -0
- package/src/renderer/App.tsx +8 -4
- package/src/renderer/api/httpClient.ts +68 -18
- package/src/renderer/api/providers.ts +23 -2
- package/src/renderer/assets/participant-avatars/01.svg +3 -0
- package/src/renderer/assets/participant-avatars/02.svg +3 -0
- package/src/renderer/assets/participant-avatars/03.svg +3 -0
- package/src/renderer/assets/participant-avatars/04.svg +3 -0
- package/src/renderer/assets/participant-avatars/05.svg +3 -0
- package/src/renderer/assets/participant-avatars/06.svg +3 -0
- package/src/renderer/assets/participant-avatars/07.svg +3 -0
- package/src/renderer/assets/participant-avatars/08.svg +3 -0
- package/src/renderer/assets/participant-avatars/09.svg +3 -0
- package/src/renderer/assets/participant-avatars/10.svg +3 -0
- package/src/renderer/assets/participant-avatars/11.svg +3 -0
- package/src/renderer/assets/participant-avatars/12.svg +3 -0
- package/src/renderer/assets/participant-avatars/13.svg +3 -0
- package/src/renderer/components/common/TerminalPane.tsx +213 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +6 -125
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
- package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
- package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
- package/src/renderer/components/extensions/plugins/PluginCard.tsx +6 -6
- package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +34 -21
- package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
- package/src/renderer/components/layout/PaneContent.tsx +8 -1
- package/src/renderer/components/layout/Sidebar.tsx +11 -54
- package/src/renderer/components/layout/SortableTab.tsx +20 -31
- package/src/renderer/components/layout/TabBar.tsx +1 -1
- package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
- package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
- package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
- package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
- package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
- package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
- package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
- package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
- package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
- package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
- package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
- package/src/renderer/components/schedules/calendar/index.ts +2 -0
- package/src/renderer/components/schedules/calendar/types.ts +44 -0
- package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
- package/src/renderer/components/settings/SettingsView.tsx +30 -35
- package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
- package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
- package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
- package/src/renderer/components/settings/sections/CliStatusSection.tsx +71 -112
- package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
- package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
- package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
- package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
- package/src/renderer/components/settings/sections/index.ts +0 -1
- package/src/renderer/components/sidebar/SidebarSessions.tsx +182 -4
- package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +39 -4
- package/src/renderer/components/splash/splashScene.ts +121 -929
- package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
- package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
- package/src/renderer/components/tasks/TasksView.tsx +112 -134
- package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
- package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
- package/src/renderer/components/team/TeamDetailView.tsx +319 -123
- package/src/renderer/components/team/TeamListView.tsx +108 -123
- package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +2 -2
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
- package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +17 -15
- package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
- package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +7 -0
- package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +1 -1
- package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
- package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
- package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
- package/src/renderer/components/team/members/MemberCard.tsx +14 -47
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
- package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
- package/src/renderer/components/team/messages/MessageComposer.tsx +8 -110
- package/src/renderer/components/team/messages/MessagesPanel.tsx +131 -114
- package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +2 -2
- package/src/renderer/components/team/tools/AddMcpInline.tsx +27 -17
- package/src/renderer/components/team/tools/McpChip.tsx +6 -3
- package/src/renderer/components/team/tools/SkillChip.tsx +2 -2
- package/src/renderer/components/team/tools/ToolsSection.tsx +418 -70
- package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
- package/src/renderer/index.css +39 -22
- package/src/renderer/index.html +17 -50
- package/src/renderer/store/index.ts +2 -1
- package/src/renderer/store/slices/scheduleSlice.ts +1 -1
- package/src/renderer/store/slices/teamSlice.ts +45 -168
- package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
- package/src/renderer/utils/memberHelpers.ts +5 -17
- package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
- package/src/renderer/utils/providerSlashCommands.ts +0 -5
- package/src/renderer/utils/scheduleFormatters.ts +3 -1
- package/src/renderer/utils/teamMessageFiltering.ts +14 -1
- package/src/renderer/utils/teamModelAvailability.ts +18 -2
- package/src/shared/types/api.ts +121 -2
- package/src/shared/types/ccConnect.ts +2 -0
- package/src/shared/types/index.ts +3 -0
- package/src/shared/types/systemManager.ts +49 -0
- package/src/shared/types/team.ts +29 -0
- package/src/shared/types/terminal.ts +4 -2
- package/src/shared/utils/extensionNormalizers.ts +15 -8
- package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
- package/dist-renderer/assets/ProjectEditorOverlay-lJZi-9Hp.js +0 -52
- package/dist-renderer/assets/channel-yIlSKy0e.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +0 -1
- package/dist-renderer/assets/clone-BTNuUva-.js +0 -1
- package/dist-renderer/assets/index-Bi6nrZ4z.css +0 -1
- package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-_m6iPPUR.js +0 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { addDays, format, isToday } from 'date-fns';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@renderer/lib/utils';
|
|
5
|
+
|
|
6
|
+
import { CalendarEventBlock } from './CalendarEventBlock';
|
|
7
|
+
import type { CalendarOccurrence } from './types';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Constants
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
const HOUR_PX = 48;
|
|
14
|
+
const GUTTER = 40;
|
|
15
|
+
|
|
16
|
+
const DAY_LABELS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// CalendarWeekView
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
interface CalendarWeekViewProps {
|
|
23
|
+
occurrences: CalendarOccurrence[];
|
|
24
|
+
weekStart: Date;
|
|
25
|
+
onEventClick: (occurrence: CalendarOccurrence) => void;
|
|
26
|
+
onSlotClick?: (date: Date) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CalendarWeekView = React.memo(function CalendarWeekView({
|
|
30
|
+
occurrences,
|
|
31
|
+
weekStart,
|
|
32
|
+
onEventClick,
|
|
33
|
+
onSlotClick,
|
|
34
|
+
}: CalendarWeekViewProps): React.JSX.Element {
|
|
35
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const [now, setNow] = useState(() => Date.now());
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const t = setInterval(() => setNow(Date.now()), 60_000);
|
|
40
|
+
return () => clearInterval(t);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!scrollRef.current) return;
|
|
45
|
+
const h = new Date().getHours();
|
|
46
|
+
scrollRef.current.scrollTop = Math.max(0, (h - 2) * HOUR_PX);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const days = useMemo(() => Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)), [weekStart]);
|
|
50
|
+
|
|
51
|
+
const byDay = useMemo(() => {
|
|
52
|
+
const m = new Map<number, CalendarOccurrence[]>();
|
|
53
|
+
for (const o of occurrences) {
|
|
54
|
+
const idx = dayDiff(o.date, weekStart);
|
|
55
|
+
if (idx < 0 || idx > 6) continue;
|
|
56
|
+
(m.get(idx) ?? m.set(idx, []).get(idx)!).push(o);
|
|
57
|
+
}
|
|
58
|
+
return m;
|
|
59
|
+
}, [occurrences, weekStart]);
|
|
60
|
+
|
|
61
|
+
const nowDate = new Date(now);
|
|
62
|
+
const nowY = ((nowDate.getHours() * 60 + nowDate.getMinutes()) / 1440) * 24 * HOUR_PX;
|
|
63
|
+
const todayIdx = dayDiff(nowDate, weekStart);
|
|
64
|
+
|
|
65
|
+
const handleSlotClick = useCallback(
|
|
66
|
+
(di: number, h: number) => {
|
|
67
|
+
if (!onSlotClick) return;
|
|
68
|
+
const d = addDays(weekStart, di);
|
|
69
|
+
d.setHours(h, 0, 0, 0);
|
|
70
|
+
onSlotClick(d);
|
|
71
|
+
},
|
|
72
|
+
[onSlotClick, weekStart],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const gridH = 24 * HOUR_PX;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="select-none">
|
|
79
|
+
{/* ── Day headers ── */}
|
|
80
|
+
<div className="flex border-b border-[var(--color-border-subtle)]" style={{ paddingLeft: GUTTER }}>
|
|
81
|
+
{days.map((day, i) => {
|
|
82
|
+
const today = isToday(day);
|
|
83
|
+
return (
|
|
84
|
+
<div key={i} className={cn('flex flex-1 items-center justify-center gap-1 py-1.5')}>
|
|
85
|
+
<span className={cn('text-[11px]', today ? 'text-[var(--color-accent)]' : 'text-[var(--color-text-muted)]')}>
|
|
86
|
+
{DAY_LABELS[i]}
|
|
87
|
+
</span>
|
|
88
|
+
<span
|
|
89
|
+
className={cn(
|
|
90
|
+
'inline-flex size-5 items-center justify-center rounded-full text-[11px] font-semibold',
|
|
91
|
+
today ? 'bg-[var(--color-accent)] text-white' : 'text-[var(--color-text)]',
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{format(day, 'd')}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* ── Grid body ── */}
|
|
102
|
+
<div ref={scrollRef} className="overflow-y-auto" style={{ maxHeight: 400 }}>
|
|
103
|
+
<div className="relative flex" style={{ height: gridH }}>
|
|
104
|
+
{/* Time gutter — only render even hours */}
|
|
105
|
+
<div className="relative shrink-0" style={{ width: GUTTER }}>
|
|
106
|
+
{Array.from({ length: 12 }, (_, i) => i * 2).map((h) => (
|
|
107
|
+
<div
|
|
108
|
+
key={h}
|
|
109
|
+
className="absolute right-0 flex items-start justify-end pr-1.5"
|
|
110
|
+
style={{ top: h * HOUR_PX, width: GUTTER }}
|
|
111
|
+
>
|
|
112
|
+
<span className="-translate-y-[6px] text-[10px] tabular-nums text-[var(--color-text-muted)]">
|
|
113
|
+
{String(h).padStart(2, '0')}:00
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* 7 day columns */}
|
|
120
|
+
{days.map((day, di) => {
|
|
121
|
+
const events = byDay.get(di) ?? [];
|
|
122
|
+
const today = isToday(day);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
key={di}
|
|
127
|
+
className={cn(
|
|
128
|
+
'relative flex-1',
|
|
129
|
+
di > 0 && 'border-l border-[var(--color-border-subtle)]/40',
|
|
130
|
+
today && 'bg-[var(--color-accent)]/[0.015]',
|
|
131
|
+
)}
|
|
132
|
+
>
|
|
133
|
+
{/* Hour lines — very subtle, only even hours */}
|
|
134
|
+
{Array.from({ length: 12 }, (_, i) => i * 2).map((h) => (
|
|
135
|
+
<div
|
|
136
|
+
key={h}
|
|
137
|
+
className="absolute left-0 right-0 border-t"
|
|
138
|
+
style={{
|
|
139
|
+
top: h * HOUR_PX,
|
|
140
|
+
borderColor: 'var(--color-border-subtle)',
|
|
141
|
+
opacity: 0.3,
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
|
|
146
|
+
{/* Events */}
|
|
147
|
+
{events.map((occ) => {
|
|
148
|
+
const topPx = ((occ.hour * 60 + occ.minute) / 1440) * gridH;
|
|
149
|
+
const hPx = Math.max((occ.durationMinutes / 1440) * gridH, 22);
|
|
150
|
+
const wPct = 100 / occ.totalColumns;
|
|
151
|
+
const lPct = occ.column * wPct;
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<CalendarEventBlock
|
|
155
|
+
key={`${occ.scheduleId}-${occ.date.toISOString()}`}
|
|
156
|
+
occurrence={occ}
|
|
157
|
+
variant="week"
|
|
158
|
+
className="absolute z-10"
|
|
159
|
+
style={{
|
|
160
|
+
top: topPx,
|
|
161
|
+
height: hPx,
|
|
162
|
+
width: `calc(${wPct}% - 2px)`,
|
|
163
|
+
left: `calc(${lPct}% + 1px)`,
|
|
164
|
+
}}
|
|
165
|
+
onClick={() => onEventClick(occ)}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
|
|
170
|
+
{/* Clickable slots */}
|
|
171
|
+
{onSlotClick &&
|
|
172
|
+
Array.from({ length: 24 }, (_, h) => (
|
|
173
|
+
<div
|
|
174
|
+
key={`s-${h}`}
|
|
175
|
+
className="absolute left-0 right-0 cursor-pointer hover:bg-white/[0.01]"
|
|
176
|
+
style={{ top: h * HOUR_PX, height: HOUR_PX }}
|
|
177
|
+
onClick={() => handleSlotClick(di, h)}
|
|
178
|
+
/>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
|
|
184
|
+
{/* Current time line */}
|
|
185
|
+
{todayIdx >= 0 && todayIdx < 7 && (
|
|
186
|
+
<div
|
|
187
|
+
className="pointer-events-none absolute z-30"
|
|
188
|
+
style={{
|
|
189
|
+
top: nowY,
|
|
190
|
+
left: GUTTER,
|
|
191
|
+
right: 0,
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<div className="relative flex" style={{ height: 0 }}>
|
|
195
|
+
{/* Position within the correct day column */}
|
|
196
|
+
<div
|
|
197
|
+
className="absolute flex items-center"
|
|
198
|
+
style={{
|
|
199
|
+
left: `${(todayIdx / 7) * 100}%`,
|
|
200
|
+
width: `${100 / 7}%`,
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<span className="size-2 shrink-0 rounded-full bg-red-500" />
|
|
204
|
+
<span className="h-[1.5px] flex-1 bg-red-500" />
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
function dayDiff(d: Date, base: Date): number {
|
|
216
|
+
const a = new Date(d); a.setHours(0,0,0,0);
|
|
217
|
+
const b = new Date(base); b.setHours(0,0,0,0);
|
|
218
|
+
return Math.round((a.getTime() - b.getTime()) / 86_400_000);
|
|
219
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Schedule } from '@shared/types';
|
|
4
|
+
|
|
5
|
+
import { TeamGanttView } from './TeamGanttView';
|
|
6
|
+
import type { CalendarViewMode } from './types';
|
|
7
|
+
|
|
8
|
+
interface ScheduleCalendarBoardProps {
|
|
9
|
+
schedules: Schedule[];
|
|
10
|
+
viewMode: CalendarViewMode;
|
|
11
|
+
onViewModeChange: (mode: CalendarViewMode) => void;
|
|
12
|
+
onEdit: (schedule: Schedule) => void;
|
|
13
|
+
onTeamClick: (teamName: string) => void;
|
|
14
|
+
getTeamColor: (teamName: string) => string;
|
|
15
|
+
getTeamDisplayName: (teamName: string) => string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ScheduleCalendarBoardInner({
|
|
19
|
+
schedules,
|
|
20
|
+
onEdit,
|
|
21
|
+
getTeamColor,
|
|
22
|
+
getTeamDisplayName,
|
|
23
|
+
}: ScheduleCalendarBoardProps) {
|
|
24
|
+
const handleEdit = useCallback(
|
|
25
|
+
(schedule: Schedule) => {
|
|
26
|
+
onEdit(schedule);
|
|
27
|
+
},
|
|
28
|
+
[onEdit],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<TeamGanttView
|
|
33
|
+
schedules={schedules}
|
|
34
|
+
getTeamColor={getTeamColor}
|
|
35
|
+
getTeamDisplayName={getTeamDisplayName}
|
|
36
|
+
onEdit={handleEdit}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ScheduleCalendarBoard = React.memo(ScheduleCalendarBoardInner);
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { addDays, addHours, format, isToday, isSameDay } from 'date-fns';
|
|
3
|
+
import { Cron } from 'croner';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@renderer/lib/utils';
|
|
6
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
import { getCronDescription } from '@renderer/utils/scheduleFormatters';
|
|
9
|
+
import type { Schedule, ScheduleStatus } from '@shared/types';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Config
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
type TimeRange = '6h' | '24h' | '3d' | '7d';
|
|
16
|
+
|
|
17
|
+
const RANGE_OPTIONS: { value: TimeRange; label: string }[] = [
|
|
18
|
+
{ value: '6h', label: '6h' },
|
|
19
|
+
{ value: '24h', label: '24h' },
|
|
20
|
+
{ value: '3d', label: '3d' },
|
|
21
|
+
{ value: '7d', label: '7d' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const RANGE_MS: Record<TimeRange, number> = {
|
|
25
|
+
'6h': 6 * 3600_000,
|
|
26
|
+
'24h': 24 * 3600_000,
|
|
27
|
+
'3d': 3 * 24 * 3600_000,
|
|
28
|
+
'7d': 7 * 24 * 3600_000,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const MAX_EXPANDED_ITEMS = 5;
|
|
32
|
+
const MAX_CRON_HITS = 200;
|
|
33
|
+
|
|
34
|
+
// Left column width for team/schedule labels
|
|
35
|
+
const LEFT_W = '140px';
|
|
36
|
+
// Minimum width for the timeline area so it doesn't cramp
|
|
37
|
+
const TIMELINE_MIN_W = '500px';
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Types
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
interface ScheduleHit {
|
|
44
|
+
schedule: Schedule;
|
|
45
|
+
date: Date;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface TeamGroup {
|
|
49
|
+
teamName: string;
|
|
50
|
+
displayName: string;
|
|
51
|
+
color: string;
|
|
52
|
+
schedules: Schedule[];
|
|
53
|
+
hits: ScheduleHit[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// TeamGanttView
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
interface TeamGanttViewProps {
|
|
61
|
+
schedules: Schedule[];
|
|
62
|
+
getTeamColor: (teamName: string) => string;
|
|
63
|
+
getTeamDisplayName: (teamName: string) => string;
|
|
64
|
+
onEdit: (schedule: Schedule) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const TeamGanttView = React.memo(function TeamGanttView({
|
|
68
|
+
schedules,
|
|
69
|
+
getTeamColor,
|
|
70
|
+
getTeamDisplayName,
|
|
71
|
+
onEdit,
|
|
72
|
+
}: TeamGanttViewProps): React.JSX.Element {
|
|
73
|
+
const [timeRange, setTimeRange] = useState<TimeRange>('24h');
|
|
74
|
+
const [expandedTeams, setExpandedTeams] = useState<Set<string>>(new Set());
|
|
75
|
+
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const rangeMs = RANGE_MS[timeRange];
|
|
78
|
+
const rangeEnd = now + rangeMs;
|
|
79
|
+
|
|
80
|
+
// Tick labels for the time axis
|
|
81
|
+
const ticks = useMemo(() => {
|
|
82
|
+
let intervalMs: number;
|
|
83
|
+
switch (timeRange) {
|
|
84
|
+
case '6h': intervalMs = 3600_000; break;
|
|
85
|
+
case '24h': intervalMs = 3 * 3600_000; break;
|
|
86
|
+
case '3d': intervalMs = 12 * 3600_000; break;
|
|
87
|
+
case '7d': intervalMs = 24 * 3600_000; break;
|
|
88
|
+
default: intervalMs = 3600_000; break;
|
|
89
|
+
}
|
|
90
|
+
const result: { time: number; pct: number }[] = [];
|
|
91
|
+
const start = Math.floor(now / intervalMs) * intervalMs;
|
|
92
|
+
for (let t = start; t <= now + rangeMs; t += intervalMs) {
|
|
93
|
+
if (t < now) continue;
|
|
94
|
+
const pct = ((t - now) / rangeMs) * 100;
|
|
95
|
+
if (pct > 100) break;
|
|
96
|
+
result.push({ time: t, pct });
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}, [now, rangeMs, timeRange]);
|
|
100
|
+
|
|
101
|
+
// Group schedules by team and compute cron hits
|
|
102
|
+
const teams = useMemo(() => {
|
|
103
|
+
const groupMap = new Map<string, TeamGroup>();
|
|
104
|
+
for (const schedule of schedules) {
|
|
105
|
+
if (schedule.status === 'disabled') continue;
|
|
106
|
+
const key = schedule.teamName;
|
|
107
|
+
if (!groupMap.has(key)) {
|
|
108
|
+
groupMap.set(key, {
|
|
109
|
+
teamName: key,
|
|
110
|
+
displayName: getTeamDisplayName(key),
|
|
111
|
+
color: getTeamColor(key),
|
|
112
|
+
schedules: [],
|
|
113
|
+
hits: [],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const group = groupMap.get(key)!;
|
|
117
|
+
group.schedules.push(schedule);
|
|
118
|
+
const hits = enumerateHits(schedule, now, rangeEnd);
|
|
119
|
+
group.hits.push(...hits);
|
|
120
|
+
}
|
|
121
|
+
return [...groupMap.values()].sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
122
|
+
}, [schedules, now, rangeEnd, getTeamColor, getTeamDisplayName]);
|
|
123
|
+
|
|
124
|
+
// Per-schedule hits for expanded rows
|
|
125
|
+
const scheduleHits = useMemo(() => {
|
|
126
|
+
const map = new Map<string, ScheduleHit[]>();
|
|
127
|
+
for (const team of teams) {
|
|
128
|
+
for (const schedule of team.schedules) {
|
|
129
|
+
map.set(schedule.id, enumerateHits(schedule, now, rangeEnd));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return map;
|
|
133
|
+
}, [teams, now, rangeEnd]);
|
|
134
|
+
|
|
135
|
+
const toggleTeam = useCallback((teamName: string) => {
|
|
136
|
+
setExpandedTeams((prev) => {
|
|
137
|
+
const next = new Set(prev);
|
|
138
|
+
if (next.has(teamName)) next.delete(teamName);
|
|
139
|
+
else next.add(teamName);
|
|
140
|
+
return next;
|
|
141
|
+
});
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="rounded-xl border border-[var(--color-border)]">
|
|
146
|
+
{/* Header */}
|
|
147
|
+
<div className="flex items-center justify-between border-b border-[var(--color-border)] px-3 py-2">
|
|
148
|
+
<span className="text-xs text-[var(--color-text-muted)]">
|
|
149
|
+
{teams.length} 个计划
|
|
150
|
+
</span>
|
|
151
|
+
<div className="inline-flex rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5">
|
|
152
|
+
{RANGE_OPTIONS.map((opt) => (
|
|
153
|
+
<button
|
|
154
|
+
key={opt.value}
|
|
155
|
+
type="button"
|
|
156
|
+
className={cn(
|
|
157
|
+
'rounded-sm px-2 py-0.5 text-[10px] transition-colors',
|
|
158
|
+
timeRange === opt.value
|
|
159
|
+
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
|
|
160
|
+
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]',
|
|
161
|
+
)}
|
|
162
|
+
onClick={() => setTimeRange(opt.value)}
|
|
163
|
+
>
|
|
164
|
+
{opt.label}
|
|
165
|
+
</button>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Time axis — aligned to grid, horizontally scrollable */}
|
|
171
|
+
<div className="overflow-x-auto border-b border-[var(--color-border)]">
|
|
172
|
+
<div className="grid" style={{ gridTemplateColumns: `${LEFT_W} minmax(${TIMELINE_MIN_W}, 1fr)`, minWidth: `calc(${LEFT_W} + ${TIMELINE_MIN_W})` }}>
|
|
173
|
+
<div className="px-3 py-1.5 text-[10px] text-[var(--color-text-muted)] opacity-50">
|
|
174
|
+
团队
|
|
175
|
+
</div>
|
|
176
|
+
<div className="relative h-6">
|
|
177
|
+
{ticks.map((tick, i) => (
|
|
178
|
+
<span
|
|
179
|
+
key={i}
|
|
180
|
+
className="absolute top-0.5 text-[10px] tabular-nums text-[var(--color-text-muted)]"
|
|
181
|
+
style={{
|
|
182
|
+
left: `${tick.pct}%`,
|
|
183
|
+
transform: 'translateX(-50%)',
|
|
184
|
+
whiteSpace: 'nowrap',
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
{formatTick(tick.time, timeRange)}
|
|
188
|
+
</span>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Team rows — scrollable horizontally */}
|
|
195
|
+
{teams.length === 0 ? (
|
|
196
|
+
<div className="flex items-center justify-center py-8 text-xs text-[var(--color-text-muted)]">
|
|
197
|
+
当前范围内没有计划任务
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<div className="overflow-x-auto">
|
|
201
|
+
{teams.map((team) => {
|
|
202
|
+
const expanded = expandedTeams.has(team.teamName);
|
|
203
|
+
const hitCount = team.hits.length;
|
|
204
|
+
const showSchedules = team.schedules.slice(0, MAX_EXPANDED_ITEMS);
|
|
205
|
+
const hiddenCount = team.schedules.length - MAX_EXPANDED_ITEMS;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div key={team.teamName} className="group/team border-b border-[var(--color-border)] last:border-b-0">
|
|
209
|
+
{/* Team row (collapsed) — grid aligned */}
|
|
210
|
+
<div
|
|
211
|
+
className="grid items-center hover:bg-white/[0.02] transition-colors"
|
|
212
|
+
style={{ gridTemplateColumns: `${LEFT_W} minmax(${TIMELINE_MIN_W}, 1fr)`, minWidth: `calc(${LEFT_W} + ${TIMELINE_MIN_W})` }}
|
|
213
|
+
>
|
|
214
|
+
{/* Left: team info */}
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
className="flex min-w-0 items-center gap-2 px-3 py-2.5 text-left"
|
|
218
|
+
onClick={() => toggleTeam(team.teamName)}
|
|
219
|
+
>
|
|
220
|
+
<span className="text-[var(--color-text-muted)]">
|
|
221
|
+
{expanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
|
222
|
+
</span>
|
|
223
|
+
<span className="size-2 shrink-0 rounded-full" style={{ backgroundColor: team.color }} />
|
|
224
|
+
<span className="truncate text-xs font-medium text-[var(--color-text)]">
|
|
225
|
+
{team.displayName}
|
|
226
|
+
</span>
|
|
227
|
+
</button>
|
|
228
|
+
|
|
229
|
+
{/* Right: mini timeline */}
|
|
230
|
+
<div className="relative h-10">
|
|
231
|
+
{/* Grid lines */}
|
|
232
|
+
{ticks.map((tick, i) => (
|
|
233
|
+
<div
|
|
234
|
+
key={i}
|
|
235
|
+
className="absolute bottom-0 top-0 w-px bg-[var(--color-border)] opacity-30"
|
|
236
|
+
style={{ left: `${tick.pct}%` }}
|
|
237
|
+
/>
|
|
238
|
+
))}
|
|
239
|
+
{/* Now marker */}
|
|
240
|
+
<div className="absolute bottom-0 top-0 w-px bg-red-500/50" style={{ left: '0%' }} />
|
|
241
|
+
|
|
242
|
+
{/* Fallback: show schedule names when no hits in range */}
|
|
243
|
+
{hitCount === 0 && team.schedules.map((schedule) => (
|
|
244
|
+
<button
|
|
245
|
+
key={schedule.id}
|
|
246
|
+
type="button"
|
|
247
|
+
className="absolute top-1/2 z-10 -translate-y-1/2 rounded-sm border border-[var(--color-border)] px-2 py-0.5 text-left transition-colors hover:bg-white/[0.04]"
|
|
248
|
+
style={{ left: '8px', transform: 'translateY(-50%)' }}
|
|
249
|
+
onClick={(e) => { e.stopPropagation(); onEdit(schedule); }}
|
|
250
|
+
>
|
|
251
|
+
<span className="text-[10px] font-medium text-[var(--color-text-secondary)]">
|
|
252
|
+
{schedule.label || getCronDescription(schedule.cronExpression)}
|
|
253
|
+
</span>
|
|
254
|
+
<span className="ml-2 text-[9px] text-[var(--color-text-muted)]">
|
|
255
|
+
{schedule.nextRunAt
|
|
256
|
+
? `下次 ${format(new Date(schedule.nextRunAt), 'M/d HH:mm')}`
|
|
257
|
+
: '无计划'}
|
|
258
|
+
</span>
|
|
259
|
+
</button>
|
|
260
|
+
))}
|
|
261
|
+
|
|
262
|
+
{/* Hits as labeled pills */}
|
|
263
|
+
{team.hits.map((hit, i) => {
|
|
264
|
+
const pct = ((hit.date.getTime() - now) / rangeMs) * 100;
|
|
265
|
+
if (pct < 0 || pct > 100) return null;
|
|
266
|
+
const isActive = hit.schedule.status === 'active';
|
|
267
|
+
return (
|
|
268
|
+
<button
|
|
269
|
+
key={`${hit.schedule.id}-${i}`}
|
|
270
|
+
type="button"
|
|
271
|
+
className={cn(
|
|
272
|
+
'absolute top-1/2 z-10 -translate-y-1/2 rounded-sm px-1.5 py-0.5 text-left transition-transform hover:scale-105 hover:z-20',
|
|
273
|
+
isActive && 'opacity-100',
|
|
274
|
+
!isActive && 'opacity-40',
|
|
275
|
+
)}
|
|
276
|
+
style={{
|
|
277
|
+
left: `${pct}%`,
|
|
278
|
+
backgroundColor: team.color,
|
|
279
|
+
maxWidth: 120,
|
|
280
|
+
transform: `translate(-50%, -50%)`,
|
|
281
|
+
}}
|
|
282
|
+
onClick={(e) => { e.stopPropagation(); onEdit(hit.schedule); }}
|
|
283
|
+
title={`${hit.schedule.label || getCronDescription(hit.schedule.cronExpression)}\n${hit.date.toLocaleString('zh-CN')}`}
|
|
284
|
+
>
|
|
285
|
+
<span className="block truncate text-[9px] font-medium leading-tight text-white">
|
|
286
|
+
{format(hit.date, 'HH:mm')}
|
|
287
|
+
</span>
|
|
288
|
+
</button>
|
|
289
|
+
);
|
|
290
|
+
})}
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{/* Expanded: per-schedule rows */}
|
|
295
|
+
{expanded && (
|
|
296
|
+
<div className="bg-[var(--color-surface-raised)]/30">
|
|
297
|
+
{showSchedules.map((schedule) => {
|
|
298
|
+
const hits = scheduleHits.get(schedule.id) ?? [];
|
|
299
|
+
const statusLabel = schedule.status === 'active' ? '运行中' : schedule.status === 'paused' ? '已暂停' : schedule.status;
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<div
|
|
303
|
+
key={schedule.id}
|
|
304
|
+
className="grid items-center hover:bg-white/[0.02] transition-colors"
|
|
305
|
+
style={{ gridTemplateColumns: `${LEFT_W} minmax(${TIMELINE_MIN_W}, 1fr)`, minWidth: `calc(${LEFT_W} + ${TIMELINE_MIN_W})` }}
|
|
306
|
+
>
|
|
307
|
+
{/* Left: schedule info */}
|
|
308
|
+
<button
|
|
309
|
+
type="button"
|
|
310
|
+
className="flex min-w-0 items-center gap-2 pl-7 pr-3 py-2 text-left"
|
|
311
|
+
onClick={() => onEdit(schedule)}
|
|
312
|
+
>
|
|
313
|
+
<span className="truncate text-[11px] text-[var(--color-text-secondary)]">
|
|
314
|
+
{schedule.label || getCronDescription(schedule.cronExpression)}
|
|
315
|
+
</span>
|
|
316
|
+
<span className={cn(
|
|
317
|
+
'shrink-0 rounded-sm px-1 py-px text-[9px]',
|
|
318
|
+
schedule.status === 'active' && 'bg-emerald-500/15 text-emerald-400',
|
|
319
|
+
schedule.status === 'paused' && 'bg-amber-500/15 text-amber-400',
|
|
320
|
+
)}>
|
|
321
|
+
{statusLabel}
|
|
322
|
+
</span>
|
|
323
|
+
</button>
|
|
324
|
+
|
|
325
|
+
{/* Right: schedule timeline */}
|
|
326
|
+
<div className="relative h-7">
|
|
327
|
+
{ticks.map((tick, i) => (
|
|
328
|
+
<div
|
|
329
|
+
key={i}
|
|
330
|
+
className="absolute bottom-0 top-0 w-px bg-[var(--color-border)] opacity-20"
|
|
331
|
+
style={{ left: `${tick.pct}%` }}
|
|
332
|
+
/>
|
|
333
|
+
))}
|
|
334
|
+
<div className="absolute bottom-0 top-0 w-px bg-red-500/30" style={{ left: '0%' }} />
|
|
335
|
+
|
|
336
|
+
{hits.map((hit, i) => {
|
|
337
|
+
const pct = ((hit.date.getTime() - now) / rangeMs) * 100;
|
|
338
|
+
if (pct < 0 || pct > 100) return null;
|
|
339
|
+
return (
|
|
340
|
+
<span
|
|
341
|
+
key={i}
|
|
342
|
+
className="absolute top-1/2 size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
|
343
|
+
style={{ left: `${pct}%`, backgroundColor: team.color }}
|
|
344
|
+
title={hit.date.toLocaleString('zh-CN')}
|
|
345
|
+
/>
|
|
346
|
+
);
|
|
347
|
+
})}
|
|
348
|
+
|
|
349
|
+
{/* Next run label */}
|
|
350
|
+
{schedule.nextRunAt && (
|
|
351
|
+
<span
|
|
352
|
+
className="absolute top-1/2 right-2 -translate-y-1/2 text-[9px] tabular-nums text-[var(--color-text-muted)]"
|
|
353
|
+
>
|
|
354
|
+
下次 {format(new Date(schedule.nextRunAt), 'HH:mm')}
|
|
355
|
+
</span>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
})}
|
|
361
|
+
{hiddenCount > 0 && (
|
|
362
|
+
<div className="pl-7 pr-3 py-1.5 text-[10px] text-[var(--color-text-muted)]">
|
|
363
|
+
+{hiddenCount} 更多
|
|
364
|
+
</div>
|
|
365
|
+
)}
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
})}
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// =============================================================================
|
|
378
|
+
// Helpers
|
|
379
|
+
// =============================================================================
|
|
380
|
+
|
|
381
|
+
function enumerateHits(schedule: Schedule, rangeStart: number, rangeEnd: number): ScheduleHit[] {
|
|
382
|
+
try {
|
|
383
|
+
const job = new Cron(schedule.cronExpression.trim(), { timezone: schedule.timezone, paused: true });
|
|
384
|
+
const raw = job.nextRuns(MAX_CRON_HITS, new Date(rangeStart));
|
|
385
|
+
const results: ScheduleHit[] = [];
|
|
386
|
+
for (const d of raw) {
|
|
387
|
+
const dt = d instanceof Date ? d : new Date(d);
|
|
388
|
+
if (dt.getTime() > rangeEnd) break;
|
|
389
|
+
if (dt.getTime() >= rangeStart) {
|
|
390
|
+
results.push({ schedule, date: dt });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return results;
|
|
394
|
+
} catch {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function formatTick(time: number, range: TimeRange): string {
|
|
400
|
+
const d = new Date(time);
|
|
401
|
+
if (range === '6h' || range === '24h') {
|
|
402
|
+
return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
403
|
+
}
|
|
404
|
+
return d.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' });
|
|
405
|
+
}
|