Skip to content

Commit 56d95f2

Browse files
committed
创建重复任务输入cron表达式是展示最近5次计划执行时间
1 parent 0c83776 commit 56d95f2

File tree

5 files changed

+198
-2
lines changed

5 files changed

+198
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,10 +436,10 @@ A: 日志保存在 `logs/` 目录下,或使用 `docker-compose logs -f` 查看
436436
- [x] 重复任务暂停功能
437437
- [x] 数据导入/导出(带加密)
438438
- [x] 任务执行日志查询
439+
- [x] 密码重置功能
439440
- [ ] 失败自动重试机制
440441
- [ ] 任务执行前后脚本钩子
441442
- [ ] 更多统计图表
442-
- [ ] 密码重置功能
443443
- [ ] Webhook 回调
444444
- [ ] 任务模板功能
445445

app.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from flask import Flask, request, jsonify, send_from_directory, Response, make_response
22
from flask_cors import CORS
3-
from datetime import datetime
3+
from datetime import datetime, timezone
44
from models import init_db, get_db, NotifyTask, NotifyChannel, NotifyStatus, User, UserChannel, ExternalCalendar
55
from scheduler import scheduler, get_cron_trigger, event_manager
66
from auth import login_required, admin_required, user_login, user_register, update_user_profile
@@ -253,6 +253,76 @@ def create_task():
253253
return jsonify({'error': str(e)}), 500
254254

255255

256+
@app.route('/api/cron/preview', methods=['POST'])
257+
@login_required
258+
def preview_cron():
259+
"""
260+
预览 Cron 表达式的未来执行时间
261+
262+
请求参数:
263+
- cron_expression: Cron 表达式字符串
264+
265+
返回:
266+
- success: 成功标志
267+
- times: 未来5次执行时间列表(格式: 2025-12-27 14:30:00)
268+
- error: 错误信息(如果有)
269+
"""
270+
try:
271+
data = request.get_json()
272+
cron_expression = data.get('cron_expression', '').strip()
273+
274+
if not cron_expression:
275+
return jsonify({
276+
'success': False,
277+
'error': 'Cron表达式不能为空'
278+
}), 400
279+
280+
try:
281+
# 使用现有的 get_cron_trigger 函数获取触发器
282+
trigger = get_cron_trigger(cron_expression)
283+
284+
# 计算未来5次执行时间
285+
times = []
286+
# 使用本地时区的当前时间
287+
import pytz
288+
local_tz = pytz.timezone('Asia/Shanghai')
289+
base_time = datetime.now(local_tz)
290+
previous_time = None
291+
292+
for _ in range(5):
293+
next_time = trigger.get_next_fire_time(previous_time, base_time)
294+
if not next_time:
295+
break
296+
# 格式化为字符串: 2025-12-27 14:30:00
297+
times.append(next_time.strftime('%Y-%m-%d %H:%M:%S'))
298+
# 重要:更新previous_time和base_time,确保下次循环计算的是之后的时间
299+
previous_time = next_time
300+
base_time = next_time
301+
302+
if not times:
303+
return jsonify({
304+
'success': False,
305+
'error': '无法计算执行时间,请检查Cron表达式'
306+
}), 400
307+
308+
return jsonify({
309+
'success': True,
310+
'times': times
311+
}), 200
312+
313+
except Exception as e:
314+
return jsonify({
315+
'success': False,
316+
'error': f'Cron表达式格式错误: {str(e)}'
317+
}), 400
318+
319+
except Exception as e:
320+
return jsonify({
321+
'success': False,
322+
'error': str(e)
323+
}), 500
324+
325+
256326
@app.route('/api/tasks', methods=['GET'])
257327
@login_required
258328
def list_tasks():

static/css/styles.css

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,61 @@
44
box-sizing: border-box;
55
}
66

7+
/* Cron表达式预览样式 */
8+
.cron-preview {
9+
margin-top: 12px;
10+
padding: 0;
11+
border-radius: var(--radius-sm);
12+
font-size: 0.9rem;
13+
line-height: 1.6;
14+
transition: all 0.3s ease;
15+
}
16+
17+
.cron-preview.success {
18+
background: linear-gradient(135deg, rgba(93, 242, 200, 0.12), rgba(93, 242, 200, 0.05));
19+
border: 1.5px solid rgba(93, 242, 200, 0.3);
20+
padding: 14px 16px;
21+
}
22+
23+
.cron-preview.error {
24+
background: linear-gradient(135deg, rgba(255, 107, 107, 0.12), rgba(255, 107, 107, 0.05));
25+
border: 1.5px solid rgba(255, 107, 107, 0.3);
26+
padding: 12px 16px;
27+
}
28+
29+
.cron-preview .preview-title {
30+
font-weight: 600;
31+
color: var(--text-strong);
32+
margin-bottom: 8px;
33+
font-size: 0.95rem;
34+
}
35+
36+
.cron-preview .preview-times {
37+
list-style: none;
38+
padding: 0;
39+
margin: 0;
40+
}
41+
42+
.cron-preview .preview-times li {
43+
padding: 6px 0;
44+
color: var(--text-strong);
45+
border-bottom: 1px solid rgba(93, 242, 200, 0.15);
46+
font-family: 'Courier New', monospace;
47+
font-size: 0.9rem;
48+
}
49+
50+
.cron-preview .preview-times li:last-child {
51+
border-bottom: none;
52+
}
53+
54+
.cron-preview .preview-error {
55+
color: #dc2626;
56+
font-weight: 500;
57+
display: flex;
58+
align-items: center;
59+
gap: 6px;
60+
}
61+
762
:root {
863
--bg: #030712;
964
--bg-accent: #0b1126;

static/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ <h2>创建通知任务</h2>
198198
<small style="color: var(--text-muted); display: block; margin-top: 5px;">
199199
示例: 0 9 * * * (每天9点) | 0 */2 * * * (每2小时) | */30 * * * * * (每30秒)
200200
</small>
201+
<div id="cronPreview" class="cron-preview"></div>
201202
</div>
202203

203204
<button type="submit" class="btn btn-primary">创建任务</button>

static/js/app.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,67 @@ let currentUser = null;
44
let userChannels = [];
55
let eventSource = null;
66

7+
// 防抖函数
8+
function debounce(func, delay) {
9+
let timeoutId;
10+
return function(...args) {
11+
clearTimeout(timeoutId);
12+
timeoutId = setTimeout(() => func.apply(this, args), delay);
13+
};
14+
}
15+
16+
// 预览Cron表达式的未来执行时间
17+
async function previewCronExpression(expression) {
18+
const previewContainer = document.getElementById('cronPreview');
19+
if (!previewContainer) return;
20+
21+
// 空值处理:清空预览区域
22+
if (!expression) {
23+
previewContainer.innerHTML = '';
24+
previewContainer.className = 'cron-preview';
25+
return;
26+
}
27+
28+
try {
29+
const token = localStorage.getItem('token');
30+
// 转换星期位:前端输入5表示星期五,需要转换为后端格式
31+
const convertedExpression = convertCronExpressionForBackend(expression);
32+
const response = await fetch(`${API_BASE}/cron/preview`, {
33+
method: 'POST',
34+
headers: {
35+
'Content-Type': 'application/json',
36+
'Authorization': `Bearer ${token}`
37+
},
38+
body: JSON.stringify({ cron_expression: convertedExpression })
39+
});
40+
41+
const data = await response.json();
42+
43+
if (data.success && data.times && data.times.length > 0) {
44+
// 显示成功预览
45+
previewContainer.className = 'cron-preview success';
46+
previewContainer.innerHTML = `
47+
<div class="preview-title">📅 未来5次执行时间:</div>
48+
<ul class="preview-times">
49+
${data.times.map((time, index) => `<li>${index + 1}. ${time}</li>`).join('')}
50+
</ul>
51+
`;
52+
} else {
53+
// 显示错误提示
54+
previewContainer.className = 'cron-preview error';
55+
previewContainer.innerHTML = `
56+
<div class="preview-error">❌ ${data.error || '无法计算执行时间'}</div>
57+
`;
58+
}
59+
} catch (error) {
60+
// 网络或其他错误
61+
previewContainer.className = 'cron-preview error';
62+
previewContainer.innerHTML = `
63+
<div class="preview-error">❌ 预览失败,请稍后重试</div>
64+
`;
65+
}
66+
}
67+
768
// 初始化
869
document.addEventListener('DOMContentLoaded', function() {
970
checkAuthStatus();
@@ -401,6 +462,15 @@ function initAppEvents() {
401462
}
402463
});
403464

465+
// Cron表达式输入事件监听(带防抖)
466+
const cronInput = document.getElementById('cronExpression');
467+
if (cronInput) {
468+
cronInput.addEventListener('input', debounce(async function(e) {
469+
const expression = e.target.value.trim();
470+
await previewCronExpression(expression);
471+
}, 300));
472+
}
473+
404474
// 绑定外部日历表单
405475
const extCalForm = document.getElementById('externalCalendarForm');
406476
if (extCalForm) {

0 commit comments

Comments
 (0)