tool.xpcool.com/src/views/cron/CronView.vue
夏犀麟 e3c1534263 feat: 初始化纯前端在线工具站
- Vite + Vue3 + Pinia + TypeScript + UnoCSS + TDesign 技术栈
- 左侧固定导航 + 暗黑/浅色主题 + PC/移动端响应式布局
- 14 个工具:图片压缩/裁剪、二维码、JSON、时间戳、Base64、
  JWT、Cron、进制转换、哈希、身份证、地址解析、URL 解析、OCR
- 本地历史记录(localStorage)与通用复制/下载/错误提示组件
- 所有文件处理均在浏览器本地完成,不上传服务器
2026-08-21 12:15:33 +08:00

286 lines
8.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-wrap">
<h2 class="page-title">Cron 表达式工具</h2>
<p class="page-desc">解析 Cron 表达式的下次执行时间或通过可视化表单生成 Cron 表达式</p>
<t-tabs v-model="tab">
<!-- ==================== 解析 ==================== -->
<t-tab-panel value="parse" label="解析表达式">
<t-card class="tool-section" title="输入表达式">
<t-input v-model="expr" placeholder="例如 0 9 * * 1-5每工作日 9 点执行)" clearable />
<div class="form-actions">
<t-button theme="primary" @click="parse">解析</t-button>
<t-button variant="outline" @click="clearParse">清空</t-button>
</div>
</t-card>
<ErrorAlert :message="error" @close="error = ''" />
<t-card v-if="nextTimes.length" class="tool-section" title="后续执行时间(北京时间)">
<div class="field-table" v-if="fields.length">
<div v-for="(f, i) in fields" :key="i" class="field-row">
<span class="field-label">{{ f.label }}</span>
<span class="field-value">{{ f.value }}</span>
</div>
</div>
<ol class="time-list">
<li v-for="(t, i) in nextTimes" :key="i">{{ t }}</li>
</ol>
<div class="form-actions">
<CopyButton :text="nextTimes.join('\n')" label="复制执行时间" />
</div>
</t-card>
</t-tab-panel>
<!-- ==================== 可视化生成 ==================== -->
<t-tab-panel value="generate" label="可视化生成">
<t-card class="tool-section" title="快捷预设">
<div class="preset-list">
<t-button v-for="p in presets" :key="p.label" variant="outline" size="small" @click="applyPreset(p.expr)">
{{ p.label }}
</t-button>
</div>
</t-card>
<t-card class="tool-section" title="字段配置">
<div class="form-row" style="margin-bottom: 10px">
<t-checkbox v-model="withSeconds">包含秒字段6 段表达式)</t-checkbox>
</div>
<div v-for="f in genFields" :key="f.key" class="form-row gen-row">
<span class="form-label">{{ f.label }}</span>
<t-select v-model="genValues[f.key]" :options="f.options" class="form-select" />
</div>
<div class="form-actions">
<t-button theme="primary" @click="buildExpr">生成表达式</t-button>
</div>
</t-card>
<ErrorAlert :message="genError" @close="genError = ''" />
<t-card v-if="genResult" class="tool-section" title="生成的表达式">
<div class="code-block">{{ genResult }}</div>
<div class="form-actions">
<CopyButton :text="genResult" label="复制表达式" />
<t-button variant="outline" size="small" @click="useGenerated">到「解析」中查看执行时间</t-button>
</div>
</t-card>
</t-tab-panel>
</t-tabs>
<HistoryPanel
:items="history.items"
@select="onHistorySelect"
@remove="history.remove"
@clear="history.clear"
/>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { parseExpression } from 'cron-parser'
import ErrorAlert from '@/components/common/ErrorAlert.vue'
import CopyButton from '@/components/common/CopyButton.vue'
import HistoryPanel from '@/components/common/HistoryPanel.vue'
import { useHistory } from '@/composables/useHistory'
import { formatBeijing } from '@/utils'
const tab = ref('parse')
const history = useHistory('cron')
/* ---------- 解析 ---------- */
const expr = ref('')
const error = ref('')
const nextTimes = ref<string[]>([])
const fields = ref<{ label: string; value: string }[]>([])
/** 字段标签6 段时首段为秒5 段时依次为分/时/日/月/周 */
function fieldLabels(segCount: number): string[] {
const base = ['分钟', '小时', '日', '月', '星期']
return segCount === 6 ? ['秒', ...base] : base
}
function parse() {
error.value = ''
nextTimes.value = []
fields.value = []
const raw = expr.value.trim()
if (!raw) {
error.value = '请输入 Cron 表达式'
return
}
try {
// cron-parser解析失败会抛异常
const interval = parseExpression(raw, { currentDate: new Date() })
const segs = raw.split(/\s+/)
fields.value = fieldLabels(segs.length).map((label, i) => ({ label, value: segs[i] ?? '' }))
const times: string[] = []
for (let i = 0; i < 6; i++) {
times.push(formatBeijing(interval.next().toDate(), true))
}
nextTimes.value = times
history.add(raw)
} catch (e) {
error.value = `表达式不合法:${e instanceof Error ? e.message : String(e)}`
}
}
function clearParse() {
expr.value = ''
error.value = ''
nextTimes.value = []
fields.value = []
}
/* ---------- 可视化生成 ---------- */
interface GenField {
key: 'second' | 'minute' | 'hour' | 'dayOfMonth' | 'month' | 'dayOfWeek'
label: string
range: [number, number]
}
const FIELD_DEFS: GenField[] = [
{ key: 'second', label: '秒', range: [0, 59] },
{ key: 'minute', label: '分钟', range: [0, 59] },
{ key: 'hour', label: '小时', range: [0, 23] },
{ key: 'dayOfMonth', label: '日', range: [1, 31] },
{ key: 'month', label: '月', range: [1, 12] },
{ key: 'dayOfWeek', label: '星期', range: [0, 6] },
]
const withSeconds = ref(false)
const genValues = reactive<Record<GenField['key'], string>>({
second: '*',
minute: '*',
hour: '*',
dayOfMonth: '*',
month: '*',
dayOfWeek: '*',
})
const genError = ref('')
const genResult = ref('')
/** 每个字段的选项:任意 + 全量取值范围(星期用 0-6 表示周日-周六) */
const genFields = computed(() =>
FIELD_DEFS.filter((f) => f.key !== 'second' || withSeconds.value).map((f) => ({
...f,
options: [
{ label: '任意(*', value: '*' },
...Array.from({ length: f.range[1] - f.range[0] + 1 }, (_, i) => {
const v = String(f.range[0] + i)
return f.key === 'dayOfWeek'
? { label: `${['日', '一', '二', '三', '四', '五', '六'][f.range[0] + i]}`, value: v }
: { label: v, value: v }
}),
],
})),
)
/** 快捷预设(标准 5 段) */
const presets = [
{ label: '每分钟', expr: '* * * * *' },
{ label: '每小时', expr: '0 * * * *' },
{ label: '每天 0 点', expr: '0 0 * * *' },
{ label: '每工作日 9 点', expr: '0 9 * * 1-5' },
{ label: '每月 1 日 0 点', expr: '0 0 1 * *' },
{ label: '每周一 9 点', expr: '0 9 * * 1' },
]
function applyPreset(e: string) {
const segs = e.split(' ')
withSeconds.value = false
genValues.minute = segs[0]
genValues.hour = segs[1]
genValues.dayOfMonth = segs[2]
genValues.month = segs[3]
genValues.dayOfWeek = segs[4]
buildExpr()
}
function buildExpr() {
genError.value = ''
genResult.value = ''
try {
const keys: GenField['key'][] = withSeconds.value
? ['second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek']
: ['minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek']
const segs = keys.map((k) => genValues[k])
const e = segs.join(' ')
// 用 cron-parser 校验生成的表达式合法性
parseExpression(e)
genResult.value = e
history.add(e)
} catch {
genError.value = '生成的表达式不合法,请检查字段配置'
}
}
function useGenerated() {
tab.value = 'parse'
expr.value = genResult.value
parse()
}
function onHistorySelect(text: string) {
tab.value = 'parse'
expr.value = text
parse()
}
</script>
<style scoped>
.form-actions {
display: flex;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}
.form-label {
width: 56px;
flex-shrink: 0;
font-size: 14px;
color: var(--td-text-color-secondary);
}
.form-select {
width: 200px;
}
.gen-row {
margin-bottom: 10px;
}
.preset-list {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.field-table {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.field-row {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 12px;
border-radius: 6px;
background: var(--td-bg-color-secondarycontainer);
}
.field-label {
font-size: 12px;
color: var(--td-text-color-placeholder);
}
.field-value {
font-size: 13px;
font-weight: 500;
color: var(--td-text-color-primary);
}
.time-list {
margin: 0;
padding-left: 20px;
font-size: 14px;
line-height: 2;
color: var(--td-text-color-primary);
}
</style>