From b7ded69a7de1d644b9b7a0e3f6cc312856d6dc67 Mon Sep 17 00:00:00 2001 From: CNWei Date: Mon, 2 Nov 2026 22:35:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(cron):=20=E7=94=A8=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=9B=BF=E4=BB=A3=E8=A7=A3=E6=9E=90=E5=99=A8?= =?UTF-8?q?=E5=8E=9F=E5=A7=8B=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 CronPreviewError 增加 user_message,预览与任务保存共用;解析器英文原文只写日志 - 针对「0/2」这类单数字步长给出具体改法(*/2 或 1-10/2) - 前端预览面板用警告图标 + 中文提示展示,网络异常回退通用文案,Cron 输入同步 aria-invalid --- backend/src/api.rs | 5 ++++- backend/src/cron.rs | 21 +++++++++++++++++++++ backend/src/tasks/service.rs | 11 ++++++----- frontend/src/App.css | 10 ++++++++++ frontend/src/components/TaskEditorModal.tsx | 14 ++++++++++---- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/backend/src/api.rs b/backend/src/api.rs index 6ff8a2c..75f1ff6 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -110,7 +110,10 @@ pub async fn preview_cron( chrono::Utc::now(), request.count, ) - .map_err(|error| ApiError::validation("expression", error.to_string()))?; + .map_err(|error| { + tracing::debug!(error = %error, "cron preview failed"); + ApiError::validation("expression", error.user_message()) + })?; Ok(Json(preview)) } diff --git a/backend/src/cron.rs b/backend/src/cron.rs index 23a7a74..8635abc 100644 --- a/backend/src/cron.rs +++ b/backend/src/cron.rs @@ -21,6 +21,27 @@ pub enum CronPreviewError { InvalidTimezone(String), } +impl CronPreviewError { + /// 面向界面的中文说明。解析器原始错误(英文、带组件路径)只写日志, + /// 不再直接抛给前端。 + pub fn user_message(&self) -> String { + match self { + Self::InvalidExpression(detail) => { + if detail.contains("Invalid step syntax") { + "Cron 表达式步长写法不正确:不要用「0/2」这种单数字步长,通配步长写「*/2」,范围步长写「1-10/2」" + .to_owned() + } else { + "Cron 表达式无效,请检查字段取值与步长写法(通配步长用 */n,范围步长用 a-b/n)" + .to_owned() + } + } + Self::TooManyRuns => format!("一次最多预览 {MAX_PREVIEW_RUNS} 次运行"), + Self::NoFutureOccurrence => "Cron 表达式没有可计算的未来执行时间".to_owned(), + Self::InvalidTimezone(_) => "时区必须是有效的 IANA 时区名称".to_owned(), + } + } +} + #[derive(Debug, Deserialize, ToSchema)] #[serde(deny_unknown_fields)] pub struct CronPreviewRequest { diff --git a/backend/src/tasks/service.rs b/backend/src/tasks/service.rs index 1346b3a..9eba333 100644 --- a/backend/src/tasks/service.rs +++ b/backend/src/tasks/service.rs @@ -166,14 +166,15 @@ fn validate_common( })?; let cron = Cron::from_str(cron_expression).map_err(|error| { - crate::error::ApiError::validation("cron", format!("Cron 表达式无效: {error}")) + tracing::debug!(error = %error, "invalid cron expression"); + let message = + crate::cron::CronPreviewError::InvalidExpression(error.to_string()).user_message(); + crate::error::ApiError::validation("cron", message) })?; cron.find_next_occurrence(&Utc::now(), false) .map_err(|error| { - crate::error::ApiError::validation( - "cron", - format!("Cron 表达式没有可计算的未来执行时间: {error}"), - ) + tracing::debug!(error = %error, "cron has no future occurrence"); + crate::error::ApiError::validation("cron", "Cron 表达式没有可计算的未来执行时间") })?; Ok(()) diff --git a/frontend/src/App.css b/frontend/src/App.css index f2e1caf..87f1da4 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -310,7 +310,17 @@ } .cron-preview-error { + display: flex; + align-items: flex-start; + gap: 8px; color: var(--su-color-danger); + font-size: var(--su-font-size-sm); + line-height: var(--su-line-height-sm); +} + +.cron-preview-error i { + flex: none; + margin-top: 2px; } .env-detector-bar { diff --git a/frontend/src/components/TaskEditorModal.tsx b/frontend/src/components/TaskEditorModal.tsx index 021431d..67a8d88 100644 --- a/frontend/src/components/TaskEditorModal.tsx +++ b/frontend/src/components/TaskEditorModal.tsx @@ -4,6 +4,7 @@ import { Button, Checkbox, FormField, + Icon, Input, Modal, Notice, @@ -290,7 +291,7 @@ export function TaskEditorModal({ {...register('cron')} className="mono-input" placeholder="0 0 2 * * *" - aria-invalid={Boolean(errors.cron)} + aria-invalid={Boolean(errors.cron) || cronPreview.isError} /> @@ -354,9 +355,14 @@ export function TaskEditorModal({ ) : cronPreview.isError ? ( - - {cronPreview.error.message} - +
+ + + {cronPreview.error instanceof ApiRequestError + ? cronPreview.error.message + : '暂时无法计算运行时间,请稍后重试'} + +
) : cronPreview.data?.runs.length ? (
{cronPreview.data.runs.map((run, index) => (