Trae Assistant commited on
Commit
3ff4c5e
·
0 Parent(s):

Initial commit: Cron Schedule Studio with robust Vue/Flask setup

Browse files
Files changed (6) hide show
  1. .gitignore +7 -0
  2. Dockerfile +10 -0
  3. README.md +53 -0
  4. app.py +26 -0
  5. requirements.txt +2 -0
  6. templates/index.html +544 -0
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .venv/
5
+ env/
6
+ venv/
7
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: 定时任务调度工坊
3
+ emoji: ⏰
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ short_description: 可视化 Cron 表达式生成与代码导出工具
9
+ ---
10
+
11
+ # 定时任务调度工坊 (Cron Schedule Studio)
12
+
13
+ 这是一个现代化的、可视化的 Cron 表达式生成器和调度工具。
14
+
15
+ ## 功能特点
16
+
17
+ - **可视化编辑器**: 直观地调整分钟、小时、日期、月份和星期。
18
+ - **实时预览**: 实时计算并显示未来 5 次运行时间(本地时间和 UTC)。
19
+ - **多格式导出**:
20
+ - Linux Crontab
21
+ - Kubernetes CronJob YAML
22
+ - GitHub Actions YAML
23
+ - Node.js (node-cron)
24
+ - Python (APScheduler)
25
+ - **暗黑模式**: 完美的深色主题支持。
26
+ - **快速预设**: 内置常用调度模板(如每天午夜、每周一等)。
27
+
28
+ ## 运行方式
29
+
30
+ ### 本地运行
31
+
32
+ 1. 安装依赖:
33
+ ```bash
34
+ pip install -r requirements.txt
35
+ ```
36
+
37
+ 2. 启动应用:
38
+ ```bash
39
+ python app.py
40
+ ```
41
+
42
+ 3. 访问: http://localhost:7860
43
+
44
+ ### Docker 运行
45
+
46
+ ```bash
47
+ docker build -t cron-schedule-studio .
48
+ docker run -p 7860:7860 cron-schedule-studio
49
+ ```
50
+
51
+ ## 许可证
52
+
53
+ MIT
app.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, send_from_directory, request
2
+ import os
3
+
4
+ app = Flask(__name__)
5
+
6
+ # Config for robustness
7
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload limit
8
+ app.config['TEMPLATES_AUTO_RELOAD'] = True
9
+
10
+ @app.route('/')
11
+ def index():
12
+ return render_template('index.html')
13
+
14
+ @app.route('/static/<path:path>')
15
+ def send_static(path):
16
+ return send_from_directory('static', path)
17
+
18
+ @app.route('/health')
19
+ def health():
20
+ return "OK", 200
21
+
22
+ if __name__ == '__main__':
23
+ port = int(os.environ.get('PORT', 7860))
24
+ # Disable debug in production-like environments if needed, but for Spaces it's fine usually.
25
+ # We use 0.0.0.0 for external access
26
+ app.run(host='0.0.0.0', port=port)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask
2
+ gunicorn
templates/index.html ADDED
@@ -0,0 +1,544 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>定时任务调度工坊 (Cron Schedule Studio)</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/cron-parser@4.9.0/dist/cron-parser.min.js"></script>
10
+ <script src="https://unpkg.com/cronstrue@latest/dist/cronstrue.min.js"></script>
11
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
12
+ <style>
13
+ body {
14
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
15
+ }
16
+ .cron-input {
17
+ transition: all 0.2s;
18
+ }
19
+ .cron-input:focus {
20
+ transform: translateY(-2px);
21
+ }
22
+ /* Custom scrollbar */
23
+ ::-webkit-scrollbar {
24
+ width: 8px;
25
+ height: 8px;
26
+ }
27
+ ::-webkit-scrollbar-track {
28
+ background: #1f2937;
29
+ }
30
+ ::-webkit-scrollbar-thumb {
31
+ background: #4b5563;
32
+ border-radius: 4px;
33
+ }
34
+ ::-webkit-scrollbar-thumb:hover {
35
+ background: #6b7280;
36
+ }
37
+ [v-cloak] { display: none; }
38
+ </style>
39
+ <script>
40
+ tailwind.config = {
41
+ darkMode: 'class',
42
+ theme: {
43
+ extend: {
44
+ colors: {
45
+ primary: '#3b82f6',
46
+ secondary: '#10b981',
47
+ dark: '#0f172a',
48
+ darker: '#020617',
49
+ surface: '#1e293b'
50
+ }
51
+ }
52
+ }
53
+ }
54
+ </script>
55
+ </head>
56
+ <body class="bg-gray-100 dark:bg-darker text-gray-800 dark:text-gray-100 min-h-screen transition-colors duration-300">
57
+ <div id="app" v-cloak class="flex flex-col min-h-screen">
58
+ <!-- Header -->
59
+ <header class="bg-white dark:bg-surface border-b dark:border-gray-700 shadow-sm sticky top-0 z-50">
60
+ <div class="container mx-auto px-4 py-3 flex justify-between items-center">
61
+ <div class="flex items-center space-x-3">
62
+ <div class="w-10 h-10 bg-gradient-to-br from-primary to-purple-600 rounded-xl flex items-center justify-center text-white shadow-lg">
63
+ <i class="fa-solid fa-clock text-xl"></i>
64
+ </div>
65
+ <div>
66
+ <h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-purple-500">
67
+ 定时任务调度工坊
68
+ </h1>
69
+ <p class="text-xs text-gray-500 dark:text-gray-400">Cron Schedule Studio</p>
70
+ </div>
71
+ </div>
72
+ <div class="flex items-center space-x-4">
73
+ <button @click="toggleTheme" class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
74
+ <i class="fas" :class="isDark ? 'fa-sun text-yellow-400' : 'fa-moon text-gray-600'"></i>
75
+ </button>
76
+ <a href="https://github.com" target="_blank" class="text-gray-500 hover:text-gray-900 dark:hover:text-white transition-colors">
77
+ <i class="fab fa-github text-xl"></i>
78
+ </a>
79
+ </div>
80
+ </div>
81
+ </header>
82
+
83
+ <!-- Main Content -->
84
+ <main class="flex-grow container mx-auto px-4 py-8">
85
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
86
+
87
+ <!-- Left Column: Editor -->
88
+ <div class="lg:col-span-2 space-y-6">
89
+
90
+ <!-- Quick Presets -->
91
+ <div class="bg-white dark:bg-surface rounded-2xl shadow-lg p-6 border border-gray-200 dark:border-gray-700">
92
+ <h2 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">快速预设</h2>
93
+ <div class="flex flex-wrap gap-2">
94
+ <button v-for="preset in presets"
95
+ @click="applyPreset(preset)"
96
+ class="px-3 py-1.5 text-sm rounded-lg border transition-all duration-200"
97
+ :class="currentPreset === preset.name
98
+ ? 'bg-primary/10 border-primary text-primary font-medium'
99
+ : 'border-gray-200 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-500'">
100
+ ${ preset.name }
101
+ </button>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Cron Editor -->
106
+ <div class="bg-white dark:bg-surface rounded-2xl shadow-lg p-6 border border-gray-200 dark:border-gray-700">
107
+ <div class="flex justify-between items-center mb-6">
108
+ <h2 class="text-xl font-bold">表达式编辑器</h2>
109
+ <div class="flex space-x-2">
110
+ <button @click="importCron" class="text-sm px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600 transition-colors mr-2">
111
+ <i class="fas fa-file-import mr-1"></i> 导入/反解析
112
+ </button>
113
+ <button @click="copyCron" class="text-sm px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
114
+ <i class="fas fa-copy mr-1"></i> 复制
115
+ </button>
116
+ <button @click="resetCron" class="text-sm px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
117
+ <i class="fas fa-undo mr-1"></i> 重置
118
+ </button>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Import Modal (Inline) -->
123
+ <div v-if="showImport" class="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 animate-fade-in">
124
+ <label class="block text-sm font-medium mb-2">输入 Cron 表达式:</label>
125
+ <div class="flex space-x-2">
126
+ <input v-model="importInput" type="text" class="flex-grow px-4 py-2 rounded-lg bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-primary outline-none" placeholder="* * * * *">
127
+ <button @click="confirmImport" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-blue-600">解析</button>
128
+ <button @click="showImport = false" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300">取消</button>
129
+ </div>
130
+ <p v-if="importError" class="text-red-500 text-xs mt-2">${ importError }</p>
131
+ </div>
132
+
133
+ <!-- The 5 Fields -->
134
+ <div class="grid grid-cols-5 gap-4 mb-8">
135
+ <div v-for="(field, index) in cronFields" :key="index" class="text-center group">
136
+ <label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-2 uppercase">${ field.label }</label>
137
+ <input type="text"
138
+ v-model="field.value"
139
+ @input="updateCronFromFields"
140
+ class="cron-input w-full text-center py-3 rounded-xl bg-gray-50 dark:bg-gray-800 border-2 border-transparent focus:border-primary focus:bg-white dark:focus:bg-gray-900 outline-none text-xl font-mono font-bold shadow-sm"
141
+ :class="{'border-red-500': field.error}">
142
+ <div class="text-[10px] text-gray-400 mt-1 opacity-0 group-hover:opacity-100 transition-opacity">${ field.range }</div>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Combined Result Display -->
147
+ <div class="bg-gray-50 dark:bg-gray-900 rounded-xl p-4 flex items-center justify-center mb-6 border border-gray-200 dark:border-gray-700">
148
+ <span class="font-mono text-3xl font-bold tracking-wider text-primary">${ cronString }</span>
149
+ </div>
150
+
151
+ <!-- Human Readable -->
152
+ <div class="text-center">
153
+ <p class="text-lg text-gray-700 dark:text-gray-300">
154
+ <i class="fas fa-language text-secondary mr-2"></i>
155
+ <span v-if="humanReadable">${ humanReadable }</span>
156
+ <span v-else class="text-red-500">表达式无效</span>
157
+ </p>
158
+ <p class="text-sm text-gray-500 mt-1">下一次运行: ${ nextRunRelative }</p>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- Next Runs -->
163
+ <div class="bg-white dark:bg-surface rounded-2xl shadow-lg p-6 border border-gray-200 dark:border-gray-700">
164
+ <h2 class="text-lg font-bold mb-4 flex items-center">
165
+ <i class="fas fa-history text-purple-500 mr-2"></i>
166
+ 未来运行时间 (Next 5 Runs)
167
+ </h2>
168
+ <div v-if="nextRuns.length > 0" class="space-y-3">
169
+ <div v-for="(run, idx) in nextRuns" :key="idx"
170
+ class="flex justify-between items-center p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors border border-gray-100 dark:border-gray-700">
171
+ <div class="flex items-center">
172
+ <span class="w-6 h-6 rounded-full bg-purple-100 dark:bg-purple-900 text-purple-600 dark:text-purple-300 flex items-center justify-center text-xs font-bold mr-3">
173
+ ${ idx + 1 }
174
+ </span>
175
+ <span class="font-mono text-sm">${ run.local }</span>
176
+ </div>
177
+ <span class="text-xs text-gray-400 font-mono">UTC: ${ run.utc }</span>
178
+ </div>
179
+ </div>
180
+ <div v-else class="text-center py-8 text-gray-400">
181
+ <i class="fas fa-exclamation-triangle mb-2 text-2xl"></i>
182
+ <p>无法计算运行时间,请检查表达式</p>
183
+ </div>
184
+ </div>
185
+
186
+ </div>
187
+
188
+ <!-- Right Column: Export & Config -->
189
+ <div class="space-y-6">
190
+
191
+ <!-- Code Export -->
192
+ <div class="bg-white dark:bg-surface rounded-2xl shadow-lg p-6 border border-gray-200 dark:border-gray-700">
193
+ <h2 class="text-lg font-bold mb-4 flex items-center">
194
+ <i class="fas fa-code text-secondary mr-2"></i>
195
+ 代码导出
196
+ </h2>
197
+
198
+ <div class="flex space-x-2 mb-4 overflow-x-auto pb-2 no-scrollbar">
199
+ <button v-for="type in exportTypes"
200
+ @click="currentExport = type.id"
201
+ class="px-3 py-1 text-xs whitespace-nowrap rounded-full transition-colors"
202
+ :class="currentExport === type.id
203
+ ? 'bg-secondary text-white shadow-md'
204
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'">
205
+ ${ type.name }
206
+ </button>
207
+ </div>
208
+
209
+ <div class="relative group">
210
+ <div class="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
211
+ <button @click="copyCode" class="p-1.5 bg-gray-700 text-white rounded hover:bg-gray-600 text-xs" title="复制">
212
+ <i class="fas fa-copy"></i>
213
+ </button>
214
+ </div>
215
+ <pre class="bg-[#282c34] text-gray-300 p-4 rounded-xl text-xs overflow-x-auto font-mono leading-relaxed custom-scrollbar"><code>${ generatedCode }</code></pre>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- History -->
220
+ <div class="bg-white dark:bg-surface rounded-2xl shadow-lg p-6 border border-gray-200 dark:border-gray-700">
221
+ <div class="flex justify-between items-center mb-4">
222
+ <h2 class="text-lg font-bold flex items-center">
223
+ <i class="fas fa-clock-rotate-left text-blue-400 mr-2"></i>
224
+ 历史记录
225
+ </h2>
226
+ <button @click="clearHistory" class="text-xs text-gray-400 hover:text-red-500">清空</button>
227
+ </div>
228
+ <div class="space-y-2 max-h-60 overflow-y-auto custom-scrollbar">
229
+ <div v-for="(hist, idx) in history" :key="idx"
230
+ @click="loadHistory(hist)"
231
+ class="p-2 bg-gray-50 dark:bg-gray-800 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer flex justify-between items-center group">
232
+ <span class="font-mono text-xs font-bold">${ hist.cron }</span>
233
+ <span class="text-[10px] text-gray-400 truncate ml-2 max-w-[100px]">${ hist.desc }</span>
234
+ </div>
235
+ <div v-if="history.length === 0" class="text-center text-sm text-gray-400 py-4">暂无历史记录</div>
236
+ </div>
237
+ </div>
238
+
239
+ <!-- Helper / Cheatsheet -->
240
+ <div class="bg-gradient-to-br from-indigo-500 to-purple-600 rounded-2xl shadow-lg p-6 text-white">
241
+ <h2 class="text-lg font-bold mb-4 flex items-center">
242
+ <i class="fas fa-lightbulb text-yellow-300 mr-2"></i>
243
+ 速查表
244
+ </h2>
245
+ <div class="space-y-3 text-sm opacity-90">
246
+ <div class="flex justify-between border-b border-white/20 pb-2">
247
+ <span>*</span>
248
+ <span>任意值</span>
249
+ </div>
250
+ <div class="flex justify-between border-b border-white/20 pb-2">
251
+ <span>,</span>
252
+ <span>分隔符 (1,3,5)</span>
253
+ </div>
254
+ <div class="flex justify-between border-b border-white/20 pb-2">
255
+ <span>-</span>
256
+ <span>范围 (1-5)</span>
257
+ </div>
258
+ <div class="flex justify-between border-b border-white/20 pb-2">
259
+ <span>/</span>
260
+ <span>步长 (*/5)</span>
261
+ </div>
262
+ </div>
263
+ <div class="mt-4 pt-4 border-t border-white/20">
264
+ <p class="text-xs italic">
265
+ 提示: 周日可以是 0 或 7。
266
+ </p>
267
+ </div>
268
+ </div>
269
+
270
+ </div>
271
+ </div>
272
+ </main>
273
+
274
+ <!-- Footer -->
275
+ <footer class="bg-white dark:bg-surface border-t dark:border-gray-700 py-6 mt-auto">
276
+ <div class="container mx-auto px-4 text-center text-sm text-gray-500">
277
+ <p>Created with <i class="fas fa-heart text-red-500 mx-1"></i> by Trae AI</p>
278
+ </div>
279
+ </footer>
280
+ </div>
281
+
282
+ <script>
283
+ const { createApp, ref, computed, watch, onMounted } = Vue;
284
+
285
+ createApp({
286
+ delimiters: ['${', '}'], // Important: Resolve conflict with Flask Jinja2
287
+ setup() {
288
+ const isDark = ref(true); // Default dark
289
+ const currentPreset = ref('');
290
+ const currentExport = ref('crontab');
291
+
292
+ const cronFields = ref([
293
+ { label: '分钟', value: '*', range: '0-59' },
294
+ { label: '小时', value: '*', range: '0-23' },
295
+ { label: '日期', value: '*', range: '1-31' },
296
+ { label: '月份', value: '*', range: '1-12' },
297
+ { label: '星期', value: '*', range: '0-7' },
298
+ ]);
299
+
300
+ const nextRuns = ref([]);
301
+ const humanReadable = ref('');
302
+ const nextRunRelative = ref('');
303
+
304
+ // Import Logic
305
+ const showImport = ref(false);
306
+ const importInput = ref('');
307
+ const importError = ref('');
308
+
309
+ // History Logic
310
+ const history = ref([]);
311
+
312
+ const presets = [
313
+ { name: '每分钟', value: '* * * * *' },
314
+ { name: '每小时', value: '0 * * * *' },
315
+ { name: '每天午夜', value: '0 0 * * *' },
316
+ { name: '每周一', value: '0 0 * * 1' },
317
+ { name: '每月1号', value: '0 0 1 * *' },
318
+ { name: '工作日(早9点)', value: '0 9 * * 1-5' },
319
+ ];
320
+
321
+ const exportTypes = [
322
+ { id: 'crontab', name: 'Linux Crontab' },
323
+ { id: 'k8s', name: 'K8s CronJob' },
324
+ { id: 'github', name: 'GitHub Actions' },
325
+ { id: 'node', name: 'Node.js' },
326
+ { id: 'python', name: 'Python' },
327
+ ];
328
+
329
+ const cronString = computed(() => {
330
+ return cronFields.value.map(f => f.value).join(' ');
331
+ });
332
+
333
+ const generatedCode = computed(() => {
334
+ const cron = cronString.value;
335
+ const desc = humanReadable.value || 'Scheduled Task';
336
+ switch(currentExport.value) {
337
+ case 'crontab':
338
+ return `# ${desc}\n# m h dom mon dow command\n${cron} /path/to/script.sh`;
339
+ case 'k8s':
340
+ return `apiVersion: batch/v1\nkind: CronJob\nmetadata:\n name: my-job\nspec:\n schedule: "${cron}"\n jobTemplate:\n spec:\n template:\n spec:\n containers:\n - name: job\n image: busybox\n args:\n - /bin/sh\n - -c\n - date; echo Hello from Kubernetes Cluster\n restartPolicy: OnFailure`;
341
+ case 'github':
342
+ return `name: Scheduled Workflow\non:\n schedule:\n - cron: '${cron}'\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout code\n uses: actions/checkout@v2\n - name: Run script\n run: echo "Hello World"`;
343
+ case 'node':
344
+ return `const cron = require('node-cron');\n\ncron.schedule('${cron}', () => {\n console.log('Running a task every ${desc}');\n});`;
345
+ case 'python':
346
+ return `# Using APScheduler:\nfrom apscheduler.schedulers.blocking import BlockingScheduler\n\nsched = BlockingScheduler()\n\n# ${desc}\n@sched.scheduled_job('cron', minute='${cronFields.value[0].value}', hour='${cronFields.value[1].value}', day='${cronFields.value[2].value}', month='${cronFields.value[3].value}', day_of_week='${cronFields.value[4].value}')\ndef scheduled_job():\n print('This job is run every ${desc}.')\n\nsched.start()`;
347
+ default:
348
+ return cron;
349
+ }
350
+ });
351
+
352
+ function applyPreset(preset) {
353
+ currentPreset.value = preset.name;
354
+ const parts = preset.value.split(' ');
355
+ if(parts.length === 5) {
356
+ cronFields.value.forEach((field, i) => {
357
+ field.value = parts[i];
358
+ });
359
+ calculate();
360
+ }
361
+ }
362
+
363
+ function updateCronFromFields() {
364
+ currentPreset.value = '';
365
+ calculate();
366
+ }
367
+
368
+ function resetCron() {
369
+ applyPreset(presets[0]);
370
+ }
371
+
372
+ // Import / Reverse Parse
373
+ function importCron() {
374
+ importInput.value = cronString.value;
375
+ importError.value = '';
376
+ showImport.value = true;
377
+ }
378
+
379
+ function confirmImport() {
380
+ const parts = importInput.value.trim().split(/\s+/);
381
+ if (parts.length !== 5) {
382
+ importError.value = 'Cron 表达式必须包含 5 个部分 (分 时 日 月 周)';
383
+ return;
384
+ }
385
+
386
+ try {
387
+ // Test if valid
388
+ const options = { currentDate: new Date() };
389
+ cronParser.parseExpression(importInput.value, options);
390
+
391
+ // Apply
392
+ cronFields.value.forEach((field, i) => {
393
+ field.value = parts[i];
394
+ });
395
+ currentPreset.value = '';
396
+ calculate();
397
+ showImport.value = false;
398
+ } catch (e) {
399
+ importError.value = '无效的 Cron 表达式: ' + e.message;
400
+ }
401
+ }
402
+
403
+ function calculate() {
404
+ try {
405
+ const options = { currentDate: new Date() };
406
+ const interval = cronParser.parseExpression(cronString.value, options);
407
+
408
+ nextRuns.value = [];
409
+ for (let i = 0; i < 5; i++) {
410
+ const date = interval.next();
411
+ nextRuns.value.push({
412
+ local: date.toString(),
413
+ utc: date.toISOString()
414
+ });
415
+ }
416
+
417
+ // Human readable
418
+ try {
419
+ humanReadable.value = cronstrue.toString(cronString.value, { locale: "zh_CN" });
420
+ } catch (e) {
421
+ humanReadable.value = "根据 Cron 表达式执行";
422
+ }
423
+
424
+ // Relative time
425
+ const next = new Date(nextRuns.value[0].local);
426
+ const now = new Date();
427
+ const diff = next - now;
428
+ const minutes = Math.floor(diff / 60000);
429
+ if (minutes < 60) nextRunRelative.value = `${minutes} 分钟后`;
430
+ else if (minutes < 1440) nextRunRelative.value = `${Math.floor(minutes/60)} 小时后`;
431
+ else nextRunRelative.value = `${Math.floor(minutes/1440)} 天后`;
432
+
433
+ cronFields.value.forEach(f => f.error = false);
434
+
435
+ addToHistory();
436
+
437
+ } catch (err) {
438
+ // console.error(err);
439
+ humanReadable.value = null;
440
+ nextRuns.value = [];
441
+ nextRunRelative.value = '';
442
+ }
443
+ }
444
+
445
+ // History Management
446
+ function addToHistory() {
447
+ const currentCron = cronString.value;
448
+ const currentDesc = humanReadable.value || 'Unknown';
449
+
450
+ // Avoid duplicates at the top
451
+ if (history.value.length > 0 && history.value[0].cron === currentCron) return;
452
+
453
+ history.value.unshift({ cron: currentCron, desc: currentDesc });
454
+ if (history.value.length > 10) history.value.pop();
455
+
456
+ localStorage.setItem('cron_history', JSON.stringify(history.value));
457
+ }
458
+
459
+ function loadHistory(hist) {
460
+ const parts = hist.cron.split(' ');
461
+ if(parts.length === 5) {
462
+ cronFields.value.forEach((field, i) => {
463
+ field.value = parts[i];
464
+ });
465
+ calculate();
466
+ }
467
+ }
468
+
469
+ function clearHistory() {
470
+ history.value = [];
471
+ localStorage.removeItem('cron_history');
472
+ }
473
+
474
+ function toggleTheme() {
475
+ isDark.value = !isDark.value;
476
+ if (isDark.value) {
477
+ document.documentElement.classList.add('dark');
478
+ } else {
479
+ document.documentElement.classList.remove('dark');
480
+ }
481
+ }
482
+
483
+ function copyCron() {
484
+ navigator.clipboard.writeText(cronString.value);
485
+ // Minimal feedback
486
+ }
487
+
488
+ function copyCode() {
489
+ navigator.clipboard.writeText(generatedCode.value);
490
+ }
491
+
492
+ onMounted(() => {
493
+ // Check system preference
494
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
495
+ isDark.value = true;
496
+ }
497
+ if (isDark.value) document.documentElement.classList.add('dark');
498
+
499
+ // Load History
500
+ const saved = localStorage.getItem('cron_history');
501
+ if (saved) {
502
+ try {
503
+ history.value = JSON.parse(saved);
504
+ } catch(e) {}
505
+ }
506
+
507
+ // Init
508
+ applyPreset(presets[2]); // Default to Daily
509
+ });
510
+
511
+ return {
512
+ isDark,
513
+ cronFields,
514
+ cronString,
515
+ nextRuns,
516
+ humanReadable,
517
+ nextRunRelative,
518
+ presets,
519
+ currentPreset,
520
+ applyPreset,
521
+ updateCronFromFields,
522
+ resetCron,
523
+ toggleTheme,
524
+ copyCron,
525
+ copyCode,
526
+ exportTypes,
527
+ currentExport,
528
+ generatedCode,
529
+ // Import
530
+ showImport,
531
+ importInput,
532
+ importError,
533
+ importCron,
534
+ confirmImport,
535
+ // History
536
+ history,
537
+ loadHistory,
538
+ clearHistory
539
+ }
540
+ }
541
+ }).mount('#app');
542
+ </script>
543
+ </body>
544
+ </html>