返回 DeepSeek-Reasonix
HeartbeatCycleEditor.tsx
根目录 / desktop / frontend / src / custom / features / heartbeat / HeartbeatCycleEditor.tsx
1 import { useCallback } from "react";
2 import type { HeartbeatTask } from "./heartbeat.types";
3 import { useHeartbeatT } from "./heartbeat.i18n";
4
5 const WEEKDAYS = [
6 { key: "mon", labelKey: "heartbeat.weekdayMon" },
7 { key: "tue", labelKey: "heartbeat.weekdayTue" },
8 { key: "wed", labelKey: "heartbeat.weekdayWed" },
9 { key: "thu", labelKey: "heartbeat.weekdayThu" },
10 { key: "fri", labelKey: "heartbeat.weekdayFri" },
11 { key: "sat", labelKey: "heartbeat.weekdaySat" },
12 { key: "sun", labelKey: "heartbeat.weekdaySun" },
13 ] as const;
14
15 const ALL_WEEKDAYS = WEEKDAYS.map(w => w.key);
16 const DEFAULT_WEEKLY_DAY = "mon";
17
18 function defaultHeartbeatCycleDays(cycleType: string): string[] {
19 if (cycleType === "daily") return [...ALL_WEEKDAYS];
20 if (cycleType === "weekly" || cycleType === "biweekly") return [DEFAULT_WEEKLY_DAY];
21 return [];
22 }
23
24 export function heartbeatBuildCycleInterval(cycleType: string, days: string[], time: string): string {
25 const base: Record<string, string> = {
26 daily: "24h",
27 weekly: "168h",
28 biweekly: "336h",
29 monthly: "720h",
30 yearly: "8760h",
31 };
32 const selectedDays = days.filter(Boolean);
33 const isDailyWithSelection = cycleType === "daily" && selectedDays.length > 0 && selectedDays.length < 7;
34 const isDailyWithoutSelection = cycleType === "daily" && selectedDays.length === 0;
35 const effectiveType = isDailyWithoutSelection || isDailyWithSelection ? "weekly" : cycleType;
36 const scheduleDays =
37 (effectiveType === "weekly" || effectiveType === "biweekly") && selectedDays.length === 0
38 ? defaultHeartbeatCycleDays(effectiveType)
39 : selectedDays;
40
41 let suffix = `|${effectiveType}`;
42 if (effectiveType === "weekly" || effectiveType === "biweekly") {
43 suffix += `:${scheduleDays.join(",")}`;
44 } else if (effectiveType === "monthly") {
45 suffix += `:${scheduleDays[0] || "1"}`;
46 } else if (effectiveType === "yearly") {
47 suffix += `:${scheduleDays[0] || "1"}-${scheduleDays[1] || "1"}`;
48 }
49 suffix += `@${time}`;
50 return (base[cycleType] || "24h") + suffix;
51 }
52
53 export function CycleEditor({
54 draft,
55 setDraft,
56 cycleType,
57 }: {
58 draft: HeartbeatTask;
59 setDraft: (field: keyof HeartbeatTask, value: string | boolean) => void;
60 cycleType: "daily" | "weekly" | "biweekly" | "monthly" | "yearly";
61 }) {
62 const t = useHeartbeatT();
63 const cycleMatch = (draft.interval || "").match(/^(\d+)[smh]\|(daily|weekly|biweekly|monthly|yearly)(?::([^@]*))?(?:@(.*))?$/);
64 const cycleDays = cycleMatch?.[3] || "";
65 // The interval is the controlled draft, including an unfinished time value.
66 const selectedDays = cycleDays ? cycleDays.split(",") : defaultHeartbeatCycleDays(cycleType);
67 const monthDay = cycleDays || "1";
68 const yearMonth = cycleDays.split("-")[0] || "1";
69 const yearDay = cycleDays.split("-")[1] || "1";
70 const timeVal = cycleMatch?.[4] ?? "09:00";
71
72 // Build interval string when config changes
73 const buildInterval = useCallback((ct: string, days: string[], tm: string) => {
74 const base: Record<string, string> = {
75 daily: "24h",
76 weekly: "168h",
77 biweekly: "336h",
78 monthly: "720h",
79 yearly: "8760h",
80 };
81 let suffix = `|${ct}`;
82 if (ct === "daily" || ct === "weekly" || ct === "biweekly") {
83 suffix += `:${days.join(",")}`;
84 } else if (ct === "monthly") {
85 suffix += `:${days[0] || "1"}`;
86 } else if (ct === "yearly") {
87 // days[0] = month, days[1] = day — each is a plain number, no dash
88 suffix += `:${days[0] || "1"}-${days[1] || "1"}`;
89 }
90 suffix += `@${tm}`;
91 return (base[ct] || "24h") + suffix;
92 }, []);
93
94 const onDayToggle = (day: string) => {
95 const isWeeklyLike = cycleType === "weekly" || cycleType === "biweekly";
96 if (selectedDays.includes(day) && selectedDays.length === 1 && isWeeklyLike) return;
97 const next = selectedDays.includes(day) ? selectedDays.filter((value) => value !== day) : [...selectedDays, day];
98 setDraft("interval", buildInterval(cycleType, next, timeVal));
99 };
100
101 const onMonthDayChange = useCallback((d: string) => {
102 setDraft("interval", buildInterval(cycleType, [d], timeVal));
103 }, [buildInterval, cycleType, setDraft, timeVal]);
104
105 const onYearMonthChange = useCallback((m: string) => {
106 setDraft("interval", buildInterval(cycleType, [m, yearDay], timeVal));
107 }, [buildInterval, cycleType, setDraft, timeVal, yearDay]);
108
109 const onYearDayChange = useCallback((d: string) => {
110 setDraft("interval", buildInterval(cycleType, [yearMonth, d], timeVal));
111 }, [buildInterval, cycleType, setDraft, timeVal, yearMonth]);
112
113 const onTimeChange = useCallback((tm: string) => {
114 const days = cycleType === "daily" || cycleType === "weekly" || cycleType === "biweekly" ? selectedDays
115 : cycleType === "monthly" ? [monthDay]
116 : cycleType === "yearly" ? [yearMonth, yearDay]
117 : [];
118 setDraft("interval", buildInterval(cycleType, days, tm));
119 }, [buildInterval, cycleType, selectedDays, monthDay, yearMonth, yearDay, setDraft]);
120
121 const MONTHS = Array.from({ length: 12 }, (_, i) => ({
122 value: String(i + 1),
123 label: t("heartbeat.monthOption", { n: i + 1 }),
124 }));
125 const DAYS = Array.from({ length: 31 }, (_, i) => ({
126 value: String(i + 1),
127 label: t("heartbeat.dayOption", { n: i + 1 }),
128 }));
129
130 return (
131 <div className="heartbeat-editor__cycle-wrap">
132 <div className="heartbeat-editor__cycle-row">
133 {cycleType === "monthly" && (
134 <select
135 className="heartbeat-editor__freq-select"
136 value={monthDay}
137 onChange={(e) => onMonthDayChange(e.target.value)}
138 >
139 {DAYS.map((d) => (
140 <option key={d.value} value={d.value}>{d.label}</option>
141 ))}
142 </select>
143 )}
144
145 {cycleType === "yearly" && (
146 <>
147 <select
148 className="heartbeat-editor__freq-select"
149 value={yearMonth}
150 onChange={(e) => onYearMonthChange(e.target.value)}
151 >
152 {MONTHS.map((m) => (
153 <option key={m.value} value={m.value}>{m.label}</option>
154 ))}
155 </select>
156 <select
157 className="heartbeat-editor__freq-select"
158 value={yearDay}
159 onChange={(e) => onYearDayChange(e.target.value)}
160 >
161 {DAYS.map((d) => (
162 <option key={d.value} value={d.value}>{d.label}</option>
163 ))}
164 </select>
165 </>
166 )}
167
168 <input
169 className="heartbeat-editor__freq-input heartbeat-editor__freq-input--time"
170 type="time"
171 value={timeVal}
172 onChange={(e) => onTimeChange(e.target.value)}
173 />
174
175 {(cycleType === "weekly" || cycleType === "biweekly") && (
176 <div className="set-seg">
177 {WEEKDAYS.map((wd) => (
178 <button
179 key={wd.key}
180 type="button"
181 className={`set-seg__btn${selectedDays.includes(wd.key) ? " set-seg__btn--on" : ""}`}
182 onClick={() => onDayToggle(wd.key)}
183 aria-pressed={selectedDays.includes(wd.key)}
184 >
185 {t(wd.labelKey)}
186 </button>
187 ))}
188 </div>
189 )}
190 </div>
191 </div>
192 );
193 }
194
194 lines Plain Text