初始化

This commit is contained in:
2026-02-22 18:56:10 +08:00
commit 26677972a6
3112 changed files with 255972 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
<script setup lang="ts">
import type { CronItemState } from './CronTabPane.vue';
import { computed, reactive, ref } from 'vue';
import {
ElButton,
ElCard,
ElDivider,
ElInput,
ElTable,
ElTableColumn,
ElTabPane,
ElTabs,
} from 'element-plus';
import { api } from '#/api/request';
import { $t } from '#/locales';
import CrontabPane from './CronTabPane.vue';
const emit = defineEmits(['useCron']);
const activeTab = ref('second');
// 默认状态工厂函数
const defaultState = (min: number, _: number): CronItemState => ({
type: 'every',
rangeStart: min,
rangeEnd: min + 1,
loopStart: min,
loopStep: 1,
specificList: [],
});
const state = reactive({
second: defaultState(0, 59),
minute: defaultState(0, 59),
hour: defaultState(0, 23),
day: { ...defaultState(1, 31), type: 'every' } as CronItemState,
month: defaultState(1, 12),
week: { ...defaultState(1, 7), type: 'none' } as CronItemState, // 默认为?
});
const weekAlias: Record<number, string> = {
1: $t('common.Sun'),
2: $t('common.Mon'),
3: $t('common.Tue'),
4: $t('common.Wed'),
5: $t('common.Thu'),
6: $t('common.Fri'),
7: $t('common.Sat'),
};
// 核心:格式化单个字段
const formatItem = (item: CronItemState): string => {
switch (item.type) {
case 'every': {
return '*';
}
case 'loop': {
return `${item.loopStart}/${item.loopStep}`;
}
case 'none': {
return '?';
}
case 'range': {
return `${item.rangeStart}-${item.rangeEnd}`;
}
case 'specific': {
if (item.specificList.length === 0) return '*';
return item.specificList.sort((a, b) => a - b).join(',');
}
default: {
return '*';
}
}
};
// 互斥逻辑
const handleDayChange = () => {
if (state.day.type !== 'none') {
state.week.type = 'none';
}
};
const handleWeekChange = () => {
if (state.week.type !== 'none') {
state.day.type = 'none';
}
};
// 计算最终 Cron 字符串
const cronResult = computed(() => {
const s = formatItem(state.second);
const m = formatItem(state.minute);
const h = formatItem(state.hour);
const d = formatItem(state.day);
const M = formatItem(state.month);
const w = formatItem(state.week);
return `${s} ${m} ${h} ${d} ${M} ${w}`;
});
// 表格展示数据
const resultTableData = computed(() => [
{
second: formatItem(state.second),
minute: formatItem(state.minute),
hour: formatItem(state.hour),
day: formatItem(state.day),
month: formatItem(state.month),
week: formatItem(state.week),
},
]);
const copyCron = () => {
// if (navigator.clipboard) {
// navigator.clipboard.writeText(cronResult.value).then(() => {
// ElMessage.success('Cron 表达式已复制');
// });
// } else {
// ElMessage.warning('浏览器不支持剪贴板 API');
// }
emit('useCron', cronResult.value);
};
const nextTimes = ref<any[]>([]);
function getNextTimes() {
api
.get('/api/v1/sysJob/getNextTimes', {
params: {
cronExpression: cronResult.value,
},
})
.then((res: any) => {
nextTimes.value = res.errorCode === 0 ? res.data : [];
});
}
</script>
<template>
<div class="cron-generator">
<ElCard class="box-card" shadow="never">
<template #header>
<div class="card-header">
<span>{{ $t('cron.cronExpressionGenerator') }}</span>
</div>
</template>
<ElTabs v-model="activeTab" type="border-card">
<!-- -->
<ElTabPane :label="$t('common.Second')" name="second">
<CrontabPane
v-model="state.second"
:min="0"
:max="59"
:label="$t('common.Second')"
/>
</ElTabPane>
<!-- -->
<ElTabPane :label="$t('common.Min')" name="minute">
<CrontabPane
v-model="state.minute"
:min="0"
:max="59"
:label="$t('common.Min')"
/>
</ElTabPane>
<!-- -->
<ElTabPane :label="$t('common.Hour')" name="hour">
<CrontabPane
v-model="state.hour"
:min="0"
:max="23"
:label="$t('common.Hour')"
/>
</ElTabPane>
<!-- -->
<ElTabPane :label="$t('common.Day')" name="day">
<CrontabPane
v-model="state.day"
:min="1"
:max="31"
:label="$t('common.Day')"
week-mode-check
@change="handleDayChange"
/>
</ElTabPane>
<!-- -->
<ElTabPane :label="$t('common.Month')" name="month">
<CrontabPane
v-model="state.month"
:min="1"
:max="12"
:label="$t('common.Month')"
/>
</ElTabPane>
<!-- -->
<ElTabPane :label="$t('common.Week')" name="week">
<CrontabPane
v-model="state.week"
:min="1"
:max="7"
:label="$t('common.Week')"
:alias-map="weekAlias"
day-mode-check
@change="handleWeekChange"
/>
</ElTabPane>
</ElTabs>
<!-- 结果展示区域 -->
<div class="result-area">
<ElDivider content-position="left">
{{ $t('cron.GenerateResult') }}
</ElDivider>
<div class="result-row">
<ElInput
v-model="cronResult"
readonly
:placeholder="$t('cron.CronExpression')"
>
<template #prepend>{{ $t('cron.CronExpression') }}</template>
</ElInput>
<ElButton type="primary" @click="copyCron" style="margin-left: 10px">
{{ $t('cron.UseThisValue') }}
</ElButton>
<ElButton
type="primary"
@click="getNextTimes"
style="margin-left: 10px"
>
{{ $t('cron.CheckLast5ExecutionTimes') }}
</ElButton>
</div>
<div class="preview-table">
<ElTable
:data="resultTableData"
border
style="width: 100%"
size="small"
>
<ElTableColumn prop="second" :label="$t('common.Second')" />
<ElTableColumn prop="minute" :label="$t('common.Min')" />
<ElTableColumn prop="hour" :label="$t('common.Hour')" />
<ElTableColumn prop="day" :label="$t('common.Day')" />
<ElTableColumn prop="month" :label="$t('common.Month')" />
<ElTableColumn prop="week" :label="$t('common.Week')" />
</ElTable>
</div>
<ElDivider content-position="left">
{{ $t('cron.Last5ExecutionTimes') }}
</ElDivider>
<div v-for="(item, idx) in nextTimes" :key="idx">
{{ item }}
</div>
</div>
</ElCard>
</div>
</template>
<style scoped>
.cron-generator {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.result-area {
margin-top: 20px;
}
.result-row {
display: flex;
margin-bottom: 15px;
}
</style>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ElButton, ElDialog } from 'element-plus';
import CronGenerator from './CronGenerator.vue';
defineProps({
modelValue: {
type: String,
default: '',
},
});
// 定义 emit 事件
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
// 使用本地变量来管理对话框显示
const showCron = ref(false);
function useCron(value: string) {
emit('update:modelValue', value);
showCron.value = false;
}
</script>
<template>
<div>
<ElDialog
draggable
title="Cron"
v-model="showCron"
width="60%"
append-to-body
>
<CronGenerator @use-cron="useCron" />
</ElDialog>
<ElButton class="mt-2" @click="showCron = true">
{{ $t('cron.ClickGenerate') }}
</ElButton>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,174 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
ElInputNumber,
ElOption,
ElRadio,
ElRadioGroup,
ElSelect,
} from 'element-plus';
// 定义接口,方便父组件引用(如果使用 TS
export interface CronItemState {
type: 'every' | 'loop' | 'none' | 'range' | 'specific';
rangeStart: number;
rangeEnd: number;
loopStart: number;
loopStep: number;
specificList: number[];
}
const props = defineProps({
modelValue: {
type: Object as () => CronItemState,
required: true,
},
label: { type: String, default: '' },
min: { type: Number, default: 0 },
max: { type: Number, default: 59 },
aliasMap: {
type: Object as () => Record<number, string>,
default: () => ({}),
}, // 用于周的转换
weekModeCheck: { type: Boolean, default: false }, // 是否是日Tab
dayModeCheck: { type: Boolean, default: false }, // 是否是周Tab
});
const emit = defineEmits(['update:modelValue', 'change']);
// 计算属性处理单选框的 v-model
const radioType = computed({
get: () => props.modelValue.type,
set: (val) => {
// 更新 type 时,触发 update
emit('update:modelValue', { ...props.modelValue, type: val });
emit('change', val);
},
});
// 通用更新函数
const updateVal = (key: keyof CronItemState, val: any) => {
emit('update:modelValue', { ...props.modelValue, [key]: val });
// 这里是否触发 change 取决于你的需求,通常数值改变不需要触发 tab 互斥检查,所以这里不一定非要 emit('change')
// 但为了保险起见,可以保留
};
// 生成下拉选项
const specificOptions = computed(() => {
const options = [];
for (let i = props.min; i <= props.max; i++) {
const label = props.aliasMap[i] ? `${props.aliasMap[i]} (${i})` : `${i}`;
options.push({ value: i, label });
}
return options;
});
</script>
<template>
<div class="crontab-pane">
<ElRadioGroup class="cron-radio-group" v-model="radioType">
<!-- 1. xxx -->
<div class="radio-line">
<ElRadio value="every">{{ $t('cron.Per') }}{{ label }} (*)</ElRadio>
</div>
<!-- 2. 不指定 (?) - 仅用于日和周 -->
<div class="radio-line" v-if="weekModeCheck || dayModeCheck">
<ElRadio value="none">{{ $t('cron.NotSpecified') }} (?)</ElRadio>
</div>
<!-- 3. 周期 (Loop) -->
<div class="radio-line">
<ElRadio value="loop">{{ $t('cron.Cycle') }}</ElRadio>
<span class="text">{{ $t('cron.From') }}</span>
<ElInputNumber
:model-value="modelValue.loopStart"
@update:model-value="(v) => updateVal('loopStart', v)"
:min="min"
:max="max"
size="small"
controls-position="right"
/>
<span class="text">{{ label }}{{ $t('cron.StartPer') }}</span>
<ElInputNumber
:model-value="modelValue.loopStep"
@update:model-value="(v) => updateVal('loopStep', v)"
:min="1"
:max="max"
size="small"
controls-position="right"
/>
<span class="text">{{ label }}{{ $t('cron.ExecuteOnce') }}</span>
</div>
<!-- 4. 区间 (Range) -->
<div class="radio-line">
<ElRadio value="range">{{ $t('cron.Rang') }}</ElRadio>
<span class="text">{{ $t('cron.From') }}</span>
<ElInputNumber
:model-value="modelValue.rangeStart"
@update:model-value="(v) => updateVal('rangeStart', v)"
:min="min"
:max="max"
size="small"
controls-position="right"
/>
<span class="text">{{ $t('cron.To') }}</span>
<ElInputNumber
:model-value="modelValue.rangeEnd"
@update:model-value="(v) => updateVal('rangeEnd', v)"
:min="min"
:max="max"
size="small"
controls-position="right"
/>
<span class="text">{{ label }}</span>
</div>
<!-- 5. 指定 (Specific) -->
<div class="radio-line">
<ElRadio value="specific">{{ $t('cron.Specify') }}</ElRadio>
<ElSelect
:model-value="modelValue.specificList"
@update:model-value="(v) => updateVal('specificList', v)"
multiple
:placeholder="$t('dictSelect.placeholder')"
style="width: 100%; min-width: 200px; margin-left: 10px"
size="small"
>
<ElOption
v-for="item in specificOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</div>
</ElRadioGroup>
</div>
</template>
<style scoped>
.cron-radio-group {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
}
.radio-line {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-bottom: 12px;
}
.text {
margin: 0 5px;
font-size: 14px;
}
:deep(.ElInputNumber) {
width: 100px;
}
</style>