用 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>

三、效果展示

相关推荐
开发者小天2 小时前
react中useReducer的使用
前端·javascript·react.js
阿蒙Amon2 小时前
JavaScript学习笔记:1.JavaScript简介
javascript·笔记·学习
小虎牙0072 小时前
关于Android Compose架构的思考
android·前端·mvvm
Irene19913 小时前
Vue3 相比 Vue2 的主要变化(生命周期、状态管理、API风格)
vue.js
Calm5503 小时前
ele表单未输入值提示为英文
前端
老华带你飞3 小时前
校务管理|基于springboot 校务管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
JosieBook3 小时前
【部署】Spring Boot + Vue框架项目生产环境部署完整方案
vue.js·spring boot·后端
爪洼守门员3 小时前
前端性能优化
开发语言·前端·javascript·笔记·性能优化