fix(schedules): 单次显示「当天」、收窄重复方式下拉

单次执行不再出现「重复方式」;「每天」改为「当天」;循环模式下拉宽度收窄。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-08 11:49:20 +08:00
parent 08a0157c95
commit 22d5d21c1f
7 changed files with 133 additions and 22 deletions
@@ -0,0 +1,41 @@
# 审计 — 调度周期「当天」文案与单次 UI(2026-06-08
**Changelog**: `docs/changelog/2026-06-08-schedule-cycle-day-label-once-ui.md`
## 审计范围
| 模块 | 文件 |
|------|------|
| 周期工具 | `frontend/src/utils/scheduleCycle.ts` |
| 选择器 | `frontend/src/components/schedules/ScheduleCyclePicker.vue` |
| 调度页 | `frontend/src/pages/SchedulesPage.vue` |
| 后端镜像 | `server/utils/schedule_cycle.py` |
| 单测 | `tests/test_schedule_cycle.py` |
## 安全
| 项 | 结论 |
|----|------|
| 无新 API | PASS — 仅文案与 UI 布局 |
| Cron 逻辑 | PASS — 未改 build/parse 规则 |
| 鉴权 | PASS — 无变更 |
## Step 3
| H | 规则 | 结论 |
|---|------|------|
| H1 | 单次不显示「重复方式」 | PASS — singleShot 固定「当天」 |
| H2 | 循环保留下拉 | PASS — 宽度 7.5rem |
| H3 | 标签「当天」 | PASS — 前后端 + 单测一致 |
## Closure
| H | 判定 |
|---|------|
| H1H3 | PASS |
## DoD
- [x] pytest tests/test_schedule_cycle.py
- [x] frontend vite build
- [x] changelog
@@ -0,0 +1,26 @@
# 2026-06-08 — 调度周期:当天文案与单次 UI
## 摘要
- 「每天」改为「当天」(列表预览同步)
- 单次执行:固定显示「当天 + 时:分」,不再出现「重复方式」下拉
- 循环执行:保留「重复方式」下拉,宽度收窄(约 7.5rem)
## 动机
用户反馈「每天」窗口过宽、语义应为当天;单次不应标注「重复方式」。
## 涉及文件
- `frontend/src/components/schedules/ScheduleCyclePicker.vue`
- `frontend/src/utils/scheduleCycle.ts`
- `server/utils/schedule_cycle.py`
- `tests/test_schedule_cycle.py`
- `frontend/src/pages/SchedulesPage.vue`
## 验证
```bash
pytest tests/test_schedule_cycle.py -q
cd frontend && npm run build
```
@@ -3,8 +3,10 @@
<div class="text-caption text-medium-emphasis mb-2">开始执行周期</div>
<v-row dense class="mb-2 align-center">
<v-col cols="12" sm="auto" style="min-width: 140px; flex: 1 1 140px">
<v-col cols="auto" class="cycle-type-col">
<span v-if="singleShot" class="cycle-type-fixed text-body-2">当天</span>
<v-select
v-else
:model-value="modelValue.type === 'custom' ? null : modelValue.type"
:items="cycleTypeOptions"
item-title="label"
@@ -13,13 +15,14 @@
variant="outlined"
density="compact"
hide-details
class="cycle-type-select"
@update:model-value="onTypeChange"
/>
</v-col>
<!-- / N / 每周 / 每月: -->
<!-- / N / 每周 / 每月: -->
<template v-if="showTimeOfDay">
<v-col cols="6" sm="auto" style="max-width: 88px">
<v-col cols="auto" class="time-col">
<v-text-field
:model-value="modelValue.hour"
label="时"
@@ -32,7 +35,7 @@
@update:model-value="patch({ hour: toNum($event, 0, 23) })"
/>
</v-col>
<v-col cols="6" sm="auto" style="max-width: 88px">
<v-col cols="auto" class="time-col">
<v-text-field
:model-value="modelValue.minute"
label="分"
@@ -48,8 +51,8 @@
</template>
<!-- 每小时 / N 小时第几分 -->
<template v-if="modelValue.type === 'hour' || modelValue.type === 'hour-n'">
<v-col cols="6" sm="auto" style="max-width: 120px">
<template v-if="!singleShot && (modelValue.type === 'hour' || modelValue.type === 'hour-n')">
<v-col cols="auto" class="time-col-wide">
<v-text-field
:model-value="modelValue.minute"
label="第几分"
@@ -71,7 +74,7 @@
<!-- N -->
<v-text-field
v-if="modelValue.type === 'day-n'"
v-if="!singleShot && modelValue.type === 'day-n'"
:model-value="modelValue.interval"
label="天数 N"
type="number"
@@ -87,7 +90,7 @@
<!-- N 小时 -->
<v-text-field
v-if="modelValue.type === 'hour-n'"
v-if="!singleShot && modelValue.type === 'hour-n'"
:model-value="modelValue.interval"
label="小时 N"
type="number"
@@ -101,7 +104,7 @@
<!-- N 分钟 -->
<v-text-field
v-if="modelValue.type === 'minute-n'"
v-if="!singleShot && modelValue.type === 'minute-n'"
:model-value="modelValue.interval"
label="分钟 N"
type="number"
@@ -117,7 +120,7 @@
<!-- 每周 -->
<v-select
v-if="modelValue.type === 'week'"
v-if="!singleShot && modelValue.type === 'week'"
:model-value="modelValue.week"
:items="WEEKDAY_OPTIONS"
item-title="label"
@@ -131,7 +134,7 @@
<!-- 每月 -->
<v-text-field
v-if="modelValue.type === 'month'"
v-if="!singleShot && modelValue.type === 'month'"
:model-value="modelValue.monthDay"
label="每月几号"
type="number"
@@ -169,7 +172,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, watch } from 'vue'
import {
CYCLE_TYPE_OPTIONS,
WEEKDAY_OPTIONS,
@@ -195,17 +198,30 @@ const cycleTypeOptions = CYCLE_TYPE_OPTIONS
const singleShot = computed(() => props.singleShot === true)
const showTimeOfDay = computed(() =>
['day', 'day-n', 'week', 'month'].includes(props.modelValue.type),
)
const showTimeOfDay = computed(() => {
if (singleShot.value) return true
return ['day', 'day-n', 'week', 'month'].includes(props.modelValue.type)
})
const cycleHint = computed(() => {
if (singleShot.value) {
return '单次:在下一匹配时刻执行一次(精确到时:分,不含日期)'
return '单次:当天指定时:分,在下一匹配时刻执行一次(不含日期选择'
}
return '循环:按周期重复执行(精确到时:分)'
return '循环:按重复方式与时:分持续执行'
})
/** 单次模式固定为「当天」时:分,不出现「重复方式」 */
watch(
() => props.singleShot,
(isOnce) => {
if (!isOnce) return
if (props.modelValue.type === 'custom') return
if (props.modelValue.type === 'day') return
emit('update:modelValue', { ...defaultCycleConfig(), ...props.modelValue, type: 'day' })
},
{ immediate: true },
)
const previewLabel = computed(() => {
try {
return formatCycleLabel(props.modelValue)
@@ -243,3 +259,31 @@ function switchToVisual() {
emit('update:modelValue', { ...defaultCycleConfig(), type: 'day' })
}
</script>
<style scoped>
.cycle-type-col {
flex: 0 0 auto;
}
.cycle-type-select {
width: 7.5rem;
max-width: 7.5rem;
}
.cycle-type-fixed {
display: inline-flex;
align-items: center;
height: 40px;
padding: 0 4px;
font-weight: 500;
white-space: nowrap;
}
.time-col {
flex: 0 0 auto;
width: 4.5rem;
max-width: 4.5rem;
}
.time-col-wide {
flex: 0 0 auto;
width: 5.5rem;
max-width: 5.5rem;
}
</style>
+1 -1
View File
@@ -105,7 +105,7 @@
/>
<v-alert type="info" density="compact" variant="tonal" class="mb-3 text-caption">
上方开始执行周期含重复方式与时:同步模式目标路径脚本超时与长任务等执行参数按系统设置及各服务器配置请在推送脚本库系统设置中调整
单次执行选当天:循环执行再选重复方式同步模式目标路径等按系统与各服务器配置
</v-alert>
<template v-if="form.schedule_type === 'push'">
+2 -2
View File
@@ -26,7 +26,7 @@ export interface ScheduleCycleConfig {
}
export const CYCLE_TYPE_OPTIONS: { value: ScheduleCycleType; label: string }[] = [
{ value: 'day', label: '天' },
{ value: 'day', label: '天' },
{ value: 'day-n', label: '每 N 天' },
{ value: 'hour', label: '每小时' },
{ value: 'hour-n', label: '每 N 小时' },
@@ -219,7 +219,7 @@ export function formatCycleLabel(input: ScheduleCycleConfig | string): string {
switch (config.type) {
case 'day':
return `${time}`
return `${time}`
case 'day-n':
return `${config.interval}${time}`
case 'hour':
+1 -1
View File
@@ -132,7 +132,7 @@ def format_cycle_label(cron_or_config: str | dict[str, Any]) -> str:
time_s = f"{hour:02d}:{minute:02d}"
if kind == "day":
return f"{time_s}"
return f"{time_s}"
if kind == "day-n":
return f"{config.get('interval', 1)}{time_s}"
if kind == "hour":
+1 -1
View File
@@ -69,7 +69,7 @@ def test_parse_cron_roundtrip_type(cron, expected_type):
def test_format_cycle_label_day():
assert format_cycle_label("0 2 * * *") == "天 02:00"
assert format_cycle_label("0 2 * * *") == "天 02:00"
def test_unknown_cron_custom():