fix(cron): 用中文说明替代解析器原始报错
- 后端 CronPreviewError 增加 user_message,预览与任务保存共用;解析器英文原文只写日志 - 针对「0/2」这类单数字步长给出具体改法(*/2 或 1-10/2) - 前端预览面板用警告图标 + 中文提示展示,网络异常回退通用文案,Cron 输入同步 aria-invalid
This commit is contained in:
+4
-1
@@ -110,7 +110,10 @@ pub async fn preview_cron(
|
|||||||
chrono::Utc::now(),
|
chrono::Utc::now(),
|
||||||
request.count,
|
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))
|
Ok(Json(preview))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ pub enum CronPreviewError {
|
|||||||
InvalidTimezone(String),
|
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)]
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct CronPreviewRequest {
|
pub struct CronPreviewRequest {
|
||||||
|
|||||||
@@ -166,14 +166,15 @@ fn validate_common(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let cron = Cron::from_str(cron_expression).map_err(|error| {
|
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)
|
cron.find_next_occurrence(&Utc::now(), false)
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
crate::error::ApiError::validation(
|
tracing::debug!(error = %error, "cron has no future occurrence");
|
||||||
"cron",
|
crate::error::ApiError::validation("cron", "Cron 表达式没有可计算的未来执行时间")
|
||||||
format!("Cron 表达式没有可计算的未来执行时间: {error}"),
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -310,7 +310,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cron-preview-error {
|
.cron-preview-error {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
color: var(--su-color-danger);
|
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 {
|
.env-detector-bar {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
FormField,
|
FormField,
|
||||||
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
Notice,
|
Notice,
|
||||||
@@ -290,7 +291,7 @@ export function TaskEditorModal({
|
|||||||
{...register('cron')}
|
{...register('cron')}
|
||||||
className="mono-input"
|
className="mono-input"
|
||||||
placeholder="0 0 2 * * *"
|
placeholder="0 0 2 * * *"
|
||||||
aria-invalid={Boolean(errors.cron)}
|
aria-invalid={Boolean(errors.cron) || cronPreview.isError}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@@ -354,9 +355,14 @@ export function TaskEditorModal({
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
) : cronPreview.isError ? (
|
) : cronPreview.isError ? (
|
||||||
<Text size="sm" className="cron-preview-error">
|
<div className="cron-preview-error">
|
||||||
{cronPreview.error.message}
|
<Icon name="warning_line" />
|
||||||
</Text>
|
<span>
|
||||||
|
{cronPreview.error instanceof ApiRequestError
|
||||||
|
? cronPreview.error.message
|
||||||
|
: '暂时无法计算运行时间,请稍后重试'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
) : cronPreview.data?.runs.length ? (
|
) : cronPreview.data?.runs.length ? (
|
||||||
<div className="cron-run-grid">
|
<div className="cron-run-grid">
|
||||||
{cronPreview.data.runs.map((run, index) => (
|
{cronPreview.data.runs.map((run, index) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user