用 Vue3 + naive-cron 开发 Cron 表达式工具:从 0 到 1 实现生成 + 反解析

作为后端、运维同学的日常刚需,Cron 表达式的编写总是让人头疼 ------*?的区别、周与日的冲突、复杂周期的拼接,每次写都得翻文档。最近我用Vue3 + naive-cron(一款开箱即用的 Cron 可视化组件),花了不到 半小时撸了个小工具:既能通过可视化面板 "点选" 生成表达式,也能把晦涩的 Cron 串反解析成人类能懂的自然语言,甚至能预览最近执行时间。分享下从构思到落地的全过程~

一、技术栈选择

  • 框架:Vue3
  • UI 组件:Naive UI(和 naive-cron 天然适配,风格统一)
  • 核心依赖:naive-cron(Cron 可视化生成 / 解析的核心组件)
  • 构建工具:Vite(开发体验拉满)

二、核心功能实现(代码片段)

核心是<naive-cron>组件,绑定v-model就能双向同步 Cron 表达式,再配合解析结果展示:

页面结构:可视化编辑器 + 结果展示代码

复制代码
<div class="cron-content">
        <!-- 表达式输入区域 -->
        <div class="expression-section">
          <n-input-group>
            <n-input
              v-model:value="cronExpression"
              placeholder="请输入或生成Cron表达式"
              style="font-family: monospace; font-size: 15px;"
              @keyup.enter="parseCron"
            />
            <n-button type="primary" @click="parseCron">
              <template #icon><n-icon><ParseIcon /></n-icon></template>
              解析
            </n-button>
            <n-button @click="addToHistory" :disabled="!cronExpression.trim()">
              <template #icon><n-icon><SaveIcon /></n-icon></template>
              保存
            </n-button>
            <n-button tertiary @click="copyExpression">
              <template #icon><n-icon><CopyIcon /></n-icon></template>
              复制
            </n-button>
          </n-input-group>
        </div>

        <n-grid :cols="24" :x-gap="20" style="margin-top: 16px;">
          <!-- 左侧:Cron编辑器 -->
          <n-gi :span="16">
            <div class="editor-wrapper">
              <div class="section-title">可视化配置器</div>
              
              <!-- 选项卡 -->
              <n-tabs v-model:value="activeTab" type="card" class="cron-tabs">
                <n-tab-pane name="second" tab="秒">
                  <div class="field-panel">
                    <n-radio-group v-model:value="fields.second.type" class="radio-horizontal">
                      <div class="radio-row">
                        <n-radio value="every">每秒执行 <span class="hint">*</span></n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="range">
                          <div class="inline-config">
                            <span>周期:从</span>
                            <n-input-number v-model:value="fields.second.range.from" :min="0" :max="59" size="small" style="width: 70px;" />
                            <span>到</span>
                            <n-input-number v-model:value="fields.second.range.to" :min="0" :max="59" size="small" style="width: 70px;" />
                            <span>秒</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="interval">
                          <div class="inline-config">
                            <span>从第</span>
                            <n-input-number v-model:value="fields.second.interval.start" :min="0" :max="59" size="small" style="width: 70px;" />
                            <span>秒开始,每隔</span>
                            <n-input-number v-model:value="fields.second.interval.step" :min="1" :max="59" size="small" style="width: 70px;" />
                            <span>秒执行</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="specific">指定秒数</n-radio>
                      </div>
                    </n-radio-group>
                    <div v-if="fields.second.type === 'specific'" class="specific-checkboxes">
                      <n-checkbox-group v-model:value="fields.second.specific">
                        <n-space :size="[12, 6]" style="flex-wrap: wrap;">
                          <n-checkbox v-for="i in 60" :key="i - 1" :value="i - 1" :label="String(i - 1)" />
                        </n-space>
                      </n-checkbox-group>
                    </div>
                  </div>
                </n-tab-pane>

                <n-tab-pane name="minute" tab="分">
                  <div class="field-panel">
                    <n-radio-group v-model:value="fields.minute.type" class="radio-horizontal">
                      <div class="radio-row">
                        <n-radio value="every">每分钟执行 <span class="hint">*</span></n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="range">
                          <div class="inline-config">
                            <span>周期:从</span>
                            <n-input-number v-model:value="fields.minute.range.from" :min="0" :max="59" size="small" style="width: 70px;" />
                            <span>到</span>
                            <n-input-number v-model:value="fields.minute.range.to" :min="0" :max="59" size="small" style="width: 70px;" />
                            <span>分</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="interval">
                          <div class="inline-config">
                            <span>从第</span>
                            <n-input-number v-model:value="fields.minute.interval.start" :min="0" :max="59" size="small" style="width: 70px;" />
                            <span>分开始,每隔</span>
                            <n-input-number v-model:value="fields.minute.interval.step" :min="1" :max="59" size="small" style="width: 70px;" />
                            <span>分钟执行</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="specific">指定分钟</n-radio>
                      </div>
                    </n-radio-group>
                    <div v-if="fields.minute.type === 'specific'" class="specific-checkboxes">
                      <n-checkbox-group v-model:value="fields.minute.specific">
                        <n-space :size="[12, 6]" style="flex-wrap: wrap;">
                          <n-checkbox v-for="i in 60" :key="i - 1" :value="i - 1" :label="String(i - 1)" />
                        </n-space>
                      </n-checkbox-group>
                    </div>
                  </div>
                </n-tab-pane>

                <n-tab-pane name="hour" tab="时">
                  <div class="field-panel">
                    <n-radio-group v-model:value="fields.hour.type" class="radio-horizontal">
                      <div class="radio-row">
                        <n-radio value="every">每小时执行 <span class="hint">*</span></n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="range">
                          <div class="inline-config">
                            <span>周期:从</span>
                            <n-input-number v-model:value="fields.hour.range.from" :min="0" :max="23" size="small" style="width: 70px;" />
                            <span>到</span>
                            <n-input-number v-model:value="fields.hour.range.to" :min="0" :max="23" size="small" style="width: 70px;" />
                            <span>点</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="interval">
                          <div class="inline-config">
                            <span>从</span>
                            <n-input-number v-model:value="fields.hour.interval.start" :min="0" :max="23" size="small" style="width: 70px;" />
                            <span>点开始,每隔</span>
                            <n-input-number v-model:value="fields.hour.interval.step" :min="1" :max="23" size="small" style="width: 70px;" />
                            <span>小时执行</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="specific">指定小时</n-radio>
                      </div>
                    </n-radio-group>
                    <div v-if="fields.hour.type === 'specific'" class="specific-checkboxes">
                      <n-checkbox-group v-model:value="fields.hour.specific">
                        <n-space :size="[12, 6]" style="flex-wrap: wrap;">
                          <n-checkbox v-for="i in 24" :key="i - 1" :value="i - 1" :label="String(i - 1)" />
                        </n-space>
                      </n-checkbox-group>
                    </div>
                  </div>
                </n-tab-pane>

                <n-tab-pane name="day" tab="日">
                  <div class="field-panel">
                    <n-radio-group v-model:value="fields.day.type" class="radio-horizontal">
                      <div class="radio-row">
                        <n-radio value="every">每天执行 <span class="hint">*</span></n-radio>
                        <n-radio value="ignore">不指定 <span class="hint">?</span></n-radio>
                        <n-radio value="last">每月最后一天 <span class="hint">L</span></n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="range">
                          <div class="inline-config">
                            <span>周期:从</span>
                            <n-input-number v-model:value="fields.day.range.from" :min="1" :max="31" size="small" style="width: 70px;" />
                            <span>到</span>
                            <n-input-number v-model:value="fields.day.range.to" :min="1" :max="31" size="small" style="width: 70px;" />
                            <span>号</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="interval">
                          <div class="inline-config">
                            <span>从</span>
                            <n-input-number v-model:value="fields.day.interval.start" :min="1" :max="31" size="small" style="width: 70px;" />
                            <span>号开始,每隔</span>
                            <n-input-number v-model:value="fields.day.interval.step" :min="1" :max="31" size="small" style="width: 70px;" />
                            <span>天执行</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="specific">指定日期</n-radio>
                      </div>
                    </n-radio-group>
                    <div v-if="fields.day.type === 'specific'" class="specific-checkboxes">
                      <n-checkbox-group v-model:value="fields.day.specific">
                        <n-space :size="[12, 6]" style="flex-wrap: wrap;">
                          <n-checkbox v-for="i in 31" :key="i" :value="i" :label="String(i)" />
                        </n-space>
                      </n-checkbox-group>
                    </div>
                  </div>
                </n-tab-pane>

                <n-tab-pane name="month" tab="月">
                  <div class="field-panel">
                    <n-radio-group v-model:value="fields.month.type" class="radio-horizontal">
                      <div class="radio-row">
                        <n-radio value="every">每月执行 <span class="hint">*</span></n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="range">
                          <div class="inline-config">
                            <span>周期:从</span>
                            <n-input-number v-model:value="fields.month.range.from" :min="1" :max="12" size="small" style="width: 70px;" />
                            <span>到</span>
                            <n-input-number v-model:value="fields.month.range.to" :min="1" :max="12" size="small" style="width: 70px;" />
                            <span>月</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="interval">
                          <div class="inline-config">
                            <span>从</span>
                            <n-input-number v-model:value="fields.month.interval.start" :min="1" :max="12" size="small" style="width: 70px;" />
                            <span>月开始,每隔</span>
                            <n-input-number v-model:value="fields.month.interval.step" :min="1" :max="12" size="small" style="width: 70px;" />
                            <span>个月执行</span>
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="specific">指定月份</n-radio>
                      </div>
                    </n-radio-group>
                    <div v-if="fields.month.type === 'specific'" class="specific-checkboxes month-checkboxes">
                      <n-checkbox-group v-model:value="fields.month.specific">
                        <n-space>
                          <n-checkbox v-for="m in monthOptions" :key="m.value" :value="m.value" :label="m.label" />
                        </n-space>
                      </n-checkbox-group>
                    </div>
                  </div>
                </n-tab-pane>

                <n-tab-pane name="week" tab="周">
                  <div class="field-panel">
                    <n-radio-group v-model:value="fields.week.type" class="radio-horizontal">
                      <div class="radio-row">
                        <n-radio value="ignore">不指定 <span class="hint">?</span></n-radio>
                        <n-radio value="every">每天执行 <span class="hint">*</span></n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="range">
                          <div class="inline-config" @click.stop>
                            <span>周期:从</span>
                            <n-select v-model:value="fields.week.range.from" :options="weekOptions" size="small" style="width: 85px;" />
                            <span>到</span>
                            <n-select v-model:value="fields.week.range.to" :options="weekOptions" size="small" style="width: 85px;" />
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="nth">
                          <div class="inline-config" @click.stop>
                            <span>第</span>
                            <n-input-number v-model:value="fields.week.nth.n" :min="1" :max="5" size="small" style="width: 80px;" />
                            <span>个</span>
                            <n-select v-model:value="fields.week.nth.day" :options="weekOptions" size="small" style="width: 85px;" />
                          </div>
                        </n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="specific">指定星期</n-radio>
                      </div>
                    </n-radio-group>
                    <div v-if="fields.week.type === 'specific'" class="specific-checkboxes week-checkboxes">
                      <n-checkbox-group v-model:value="fields.week.specific">
                        <n-space>
                          <n-checkbox v-for="w in weekOptions" :key="w.value" :value="w.value" :label="w.label" />
                        </n-space>
                      </n-checkbox-group>
                    </div>
                  </div>
                </n-tab-pane>

                <n-tab-pane name="year" tab="年">
                  <div class="field-panel">
                    <n-radio-group v-model:value="fields.year.type" class="radio-horizontal">
                      <div class="radio-row">
                        <n-radio value="empty">不填写 <span class="hint">留空</span></n-radio>
                        <n-radio value="every">每年执行 <span class="hint">*</span></n-radio>
                      </div>
                      <div class="radio-row">
                        <n-radio value="range">
                          <div class="inline-config">
                            <span>周期:从</span>
                            <n-input-number v-model:value="fields.year.range.from" :min="2020" :max="2099" size="small" style="width: 90px;" />
                            <span>到</span>
                            <n-input-number v-model:value="fields.year.range.to" :min="2020" :max="2099" size="small" style="width: 90px;" />
                            <span>年</span>
                          </div>
                        </n-radio>
                      </div>
                    </n-radio-group>
                  </div>
                </n-tab-pane>
              </n-tabs>

              <!-- 表达式结果展示 -->
              <div class="result-table">
                <n-table :bordered="false" :single-line="false" size="small">
                  <thead>
                    <tr>
                      <th>秒</th><th>分</th><th>时</th><th>日</th><th>月</th><th>周</th><th>年</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr>
                      <td><code>{{ computedFields.second }}</code></td>
                      <td><code>{{ computedFields.minute }}</code></td>
                      <td><code>{{ computedFields.hour }}</code></td>
                      <td><code>{{ computedFields.day }}</code></td>
                      <td><code>{{ computedFields.month }}</code></td>
                      <td><code>{{ computedFields.week }}</code></td>
                      <td><code>{{ computedFields.year || '-' }}</code></td>
                    </tr>
                  </tbody>
                </n-table>
              </div>
            </div>

            <!-- 表达式说明 -->
            <div class="description-section" v-if="cronDescription">
              <div class="section-title">表达式说明</div>
              <n-alert type="info" :show-icon="false" style="border-radius: 8px;">
                {{ cronDescription }}
              </n-alert>
            </div>

            <!-- 执行时间预览 -->
            <div class="preview-section">
              <div class="section-title">执行时间预览(近{{ executionTimes.length }}次)</div>
              <n-spin :show="parsing">
                <div class="execution-times">
                  <n-empty v-if="executionTimes.length === 0" description="请输入Cron表达式并解析" size="small" />
                  <div v-else class="time-list">
                    <div v-for="(time, index) in executionTimes" :key="index" class="time-item">
                      <n-icon color="#18a058" size="14"><CheckIcon /></n-icon>
                      <span>{{ time }}</span>
                    </div>
                  </div>
                </div>
              </n-spin>
            </div>
          </n-gi>

          <!-- 右侧:示例和历史 -->
          <n-gi :span="8">
            <div class="side-section" style="">
              <div class="section-title">
                <span>常用示例</span>
                <n-button text size="small" type="info" @click="showHelpModal = true">
                  表达式详解
                </n-button>
              </div>
              <div class="example-list">
                <div
                  v-for="example in cronExamples"
                  :key="example.expression"
                  class="example-item"
                  @click="applyExample(example)"
                >
                  <code class="example-code">{{ example.expression }}</code>
                  <span class="example-desc">{{ example.description }}</span>
                </div>
              </div>
            </div>

            <div class="side-section" style="margin-top: 16px;">
              <div class="section-title">
                <span>历史记录</span>
                <n-button text size="small" type="error" @click="clearHistory" v-if="historyList.length > 0">
                  清空
                </n-button>
              </div>
              <div class="history-list">
                <n-empty v-if="historyList.length === 0" description="暂无历史记录" size="small" />
                <div
                  v-for="(item, index) in historyList"
                  :key="index"
                  class="history-item"
                >
                  <div class="history-content" @click="applyHistory(item)">
                    <code class="history-code">{{ item.expression }}</code>
                    <span class="history-time">{{ item.time }}</span>
                  </div>
                  <n-button text size="tiny" type="error" @click.stop="removeHistory(index)">
                    <n-icon><DeleteIcon /></n-icon>
                  </n-button>
                </div>
              </div>
            </div>
          </n-gi>
        </n-grid>
      </div>
    </n-card>

三、效果展示

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax