diff --git a/frontend/src/App.css b/frontend/src/App.css index e677867..f2e1caf 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -211,10 +211,6 @@ gap: 16px; } -.form-grid-single { - grid-template-columns: 1fr; -} - .form-select { width: 100%; } @@ -236,15 +232,13 @@ flex: 1; } -.cron-preview-box { +.cron-preview-panel { display: flex; - max-height: 160px; flex-direction: column; - gap: 6px; - overflow-y: auto; + gap: 10px; border: 1px solid var(--su-border); border-radius: var(--su-radius-sm); - padding: 10px 12px; + padding: 12px 14px; background-color: var(--su-bg-subtle); } @@ -252,15 +246,42 @@ display: flex; align-items: center; justify-content: space-between; - gap: 12px; + gap: 16px; + flex-wrap: wrap; +} + +.cron-preview-title { + display: flex; + flex-direction: column; + gap: 2px; +} + +.cron-preview-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.cron-preview-status { + display: flex; + align-items: center; + gap: 8px; } .cron-count-select { min-width: 96px; } +/* 未来运行时间按三列铺开:默认 5 条一眼看完,不需要内部滚动条 */ +.cron-run-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 6px 12px; +} + .cron-run-item { display: flex; + min-width: 0; align-items: center; gap: 8px; color: var(--su-text-secondary); @@ -270,18 +291,22 @@ .cron-run-index { width: 28px; + flex: none; color: var(--su-text-muted); font-size: 11px; text-align: right; } .cron-run-time { + overflow: hidden; border: 1px solid var(--su-border); border-radius: var(--su-radius-xs); padding: 2px 8px; background-color: var(--su-bg-card); color: var(--su-text-main); font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; } .cron-preview-error { @@ -854,6 +879,10 @@ grid-template-columns: 1fr; } + .cron-run-grid { + grid-template-columns: 1fr; + } + .argument-row, .key-value-row { grid-template-columns: 1fr; diff --git a/frontend/src/components/TaskEditorModal.tsx b/frontend/src/components/TaskEditorModal.tsx index 5092713..021431d 100644 --- a/frontend/src/components/TaskEditorModal.tsx +++ b/frontend/src/components/TaskEditorModal.tsx @@ -9,6 +9,7 @@ import { Notice, PasswordInput, Select, + Spinner, Text, Textarea, TextButton, @@ -97,7 +98,10 @@ export function TaskEditorModal({ } = useForm({ resolver: zodResolver(taskFormSchema), defaultValues: createEmptyTaskFormValues(), - mode: 'onBlur', + // 长表单在 blur 时插错误行会让下面所有控件瞬间位移,刚点下去的按钮 + // 可能在 mouseup 前被挪走(下拉框会「点了没反应」)。改为提交时校验, + // 提交后仍按 onChange 实时修正。 + mode: 'onSubmit', }) const actionType = watch('actionType') const cronValue = watch('cron') @@ -288,42 +292,6 @@ export function TaskEditorModal({ placeholder="0 0 2 * * *" aria-invalid={Boolean(errors.cron)} /> -
-
- - 预览未来运行时间 - - ({ + value: String(count), + label: `${count} 次`, + }))} + value={String(cronPreviewCount)} + onChange={(value) => setCronPreviewCount(Number(value))} + /> +
+
+ + {cronPreview.isFetching ? ( +
+ + + 正在计算... + +
+ ) : cronPreview.isError ? ( + + {cronPreview.error.message} + + ) : cronPreview.data?.runs.length ? ( +
+ {cronPreview.data.runs.map((run, index) => ( +
+ #{index + 1} + {run} +
+ ))} +
+ ) : ( + + 等待输入有效 Cron 表达式... + + )} + + +