初始化
This commit is contained in:
281
easyflow-ui-admin/app/src/components/cron/CronGenerator.vue
Normal file
281
easyflow-ui-admin/app/src/components/cron/CronGenerator.vue
Normal 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>
|
||||
44
easyflow-ui-admin/app/src/components/cron/CronPicker.vue
Normal file
44
easyflow-ui-admin/app/src/components/cron/CronPicker.vue
Normal 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>
|
||||
174
easyflow-ui-admin/app/src/components/cron/CronTabPane.vue
Normal file
174
easyflow-ui-admin/app/src/components/cron/CronTabPane.vue
Normal 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>
|
||||
Reference in New Issue
Block a user