Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import CronParser from 'cron-parser';
|
||||
import { ElInput, ElTabPane, ElTabs, ElTooltip } from 'element-plus';
|
||||
|
||||
import DayUI from './tabs/DayUI.vue';
|
||||
import HourUI from './tabs/HourUI.vue';
|
||||
import MinuteUI from './tabs/MinuteUI.vue';
|
||||
import MonthUI from './tabs/MonthUI.vue';
|
||||
import SecondUI from './tabs/SecondUI.vue';
|
||||
import WeekUI from './tabs/WeekUI.vue';
|
||||
import YearUI from './tabs/YearUI.vue';
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
disabled?: boolean;
|
||||
hideSecond?: boolean;
|
||||
hideYear?: boolean;
|
||||
remote?: (
|
||||
cron: string,
|
||||
timestamp: number,
|
||||
callback: (result: string) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
disabled: false,
|
||||
hideSecond: true,
|
||||
hideYear: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
provide('prefixCls', 'cron');
|
||||
|
||||
const activeKey = ref('minute');
|
||||
const second = ref('*');
|
||||
const minute = ref('*');
|
||||
const hour = ref('*');
|
||||
const day = ref('*');
|
||||
const month = ref('*');
|
||||
const week = ref('*');
|
||||
const year = ref('*');
|
||||
|
||||
const inputValues = reactive({
|
||||
second: '',
|
||||
minute: '',
|
||||
hour: '',
|
||||
day: '',
|
||||
month: '',
|
||||
week: '',
|
||||
year: '',
|
||||
cron: '',
|
||||
});
|
||||
|
||||
const preTimeList = ref('执行预览,会忽略年份参数。');
|
||||
|
||||
// 计算 cron 表达式
|
||||
const cronValueInner = computed(() => {
|
||||
const result: string[] = [];
|
||||
if (!props.hideSecond) {
|
||||
result.push(second.value ? second.value : '*');
|
||||
}
|
||||
result.push(
|
||||
minute.value ? minute.value : '*',
|
||||
hour.value ? hour.value : '*',
|
||||
day.value ? day.value : '*',
|
||||
month.value ? month.value : '*',
|
||||
week.value ? week.value : '*',
|
||||
);
|
||||
if (!props.hideYear && !props.hideSecond)
|
||||
result.push(year.value ? year.value : '*');
|
||||
return result.join(' ');
|
||||
});
|
||||
|
||||
// 不含年的 cron 表达式
|
||||
const cronValueNoYear = computed(() => {
|
||||
const v = cronValueInner.value;
|
||||
if (props.hideYear || props.hideSecond) return v;
|
||||
let vs = v.split(' ');
|
||||
if (vs.length >= 5) {
|
||||
// 转成 Quartz 的规则
|
||||
vs = ['0', vs[0], vs[1], vs[2], vs[3], convertWeekToQuartz(vs[4]), '*'];
|
||||
}
|
||||
return vs.slice(0, -1).join(' ');
|
||||
});
|
||||
|
||||
const calTriggerList = useDebounceFn(calTriggerListInner, 500);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal === cronValueInner.value) {
|
||||
return;
|
||||
}
|
||||
formatValue();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(cronValueInner, (newValue) => {
|
||||
calTriggerList();
|
||||
emitValue(newValue);
|
||||
assignInput();
|
||||
});
|
||||
|
||||
assignInput();
|
||||
formatValue();
|
||||
calTriggerListInner();
|
||||
|
||||
function assignInput() {
|
||||
inputValues.second = second.value;
|
||||
inputValues.minute = minute.value;
|
||||
inputValues.hour = hour.value;
|
||||
inputValues.day = day.value;
|
||||
inputValues.month = month.value;
|
||||
inputValues.week = week.value;
|
||||
inputValues.year = year.value;
|
||||
inputValues.cron = cronValueInner.value;
|
||||
if (!props.modelValue) emitValue(inputValues.cron);
|
||||
}
|
||||
|
||||
function formatValue() {
|
||||
if (!props.modelValue) return;
|
||||
const values = props.modelValue.split(' ').filter((item) => !!item);
|
||||
if (!values || values.length <= 0) return;
|
||||
let i = 0;
|
||||
if (!props.hideSecond) second.value = values[i++];
|
||||
if (values.length > i) minute.value = values[i++];
|
||||
if (values.length > i) hour.value = values[i++];
|
||||
if (values.length > i) day.value = values[i++];
|
||||
if (values.length > i) month.value = values[i++];
|
||||
if (values.length > i) week.value = values[i++];
|
||||
if (values.length > i) year.value = values[i];
|
||||
assignInput();
|
||||
}
|
||||
|
||||
// Quartz 的规则:
|
||||
// 1 = 周日,2 = 周一,3 = 周二,4 = 周三,5 = 周四,6 = 周五,7 = 周六
|
||||
function convertWeekToQuartz(week: string) {
|
||||
const convert = (v: string) => {
|
||||
if (v === '0') {
|
||||
return '1';
|
||||
}
|
||||
if (v === '1') {
|
||||
return '0';
|
||||
}
|
||||
return (Number.parseInt(v) - 1).toString();
|
||||
};
|
||||
|
||||
const patten1 = /^([0-7])([-/])([0-7])$/;
|
||||
const patten2 = /^([0-7])(,[0-7])+$/;
|
||||
|
||||
if (/^[0-7]$/.test(week)) {
|
||||
return convert(week);
|
||||
} else if (patten1.test(week)) {
|
||||
return week.replace(patten1, (_$0, before, separator, after) => {
|
||||
return separator === '/'
|
||||
? convert(before) + separator + after
|
||||
: convert(before) + separator + convert(after);
|
||||
});
|
||||
} else if (patten2.test(week)) {
|
||||
return week
|
||||
.split(',')
|
||||
.map((v) => convert(v))
|
||||
.join(',');
|
||||
}
|
||||
return week;
|
||||
}
|
||||
|
||||
function calTriggerListInner() {
|
||||
if (props.remote) {
|
||||
props.remote(cronValueInner.value, Date.now(), (v) => {
|
||||
preTimeList.value = v;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
currentDate: new Date(),
|
||||
};
|
||||
|
||||
try {
|
||||
const iter = CronParser.parseExpression(cronValueNoYear.value, options);
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const nextDate = iter.next();
|
||||
result.push(nextDate.toDate().toLocaleString());
|
||||
}
|
||||
preTimeList.value = result.length > 0 ? result.join('\n') : '无执行时间';
|
||||
} catch {
|
||||
preTimeList.value = '无效的Cron表达式';
|
||||
}
|
||||
}
|
||||
|
||||
function onInputBlur() {
|
||||
second.value = inputValues.second;
|
||||
minute.value = inputValues.minute;
|
||||
hour.value = inputValues.hour;
|
||||
day.value = inputValues.day;
|
||||
month.value = inputValues.month;
|
||||
week.value = inputValues.week;
|
||||
year.value = inputValues.year;
|
||||
}
|
||||
|
||||
function onInputCronBlur() {
|
||||
emitValue(inputValues.cron);
|
||||
}
|
||||
|
||||
function emitValue(value: string) {
|
||||
emit('change', value);
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-inner">
|
||||
<ElTabs v-model="activeKey">
|
||||
<ElTabPane v-if="!hideSecond" label="秒" name="second">
|
||||
<SecondUI v-model="second" :disabled="disabled" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="分" name="minute">
|
||||
<MinuteUI v-model="minute" :disabled="disabled" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="时" name="hour">
|
||||
<HourUI v-model="hour" :disabled="disabled" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="日" name="day">
|
||||
<DayUI v-model="day" :week="week" :disabled="disabled" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="月" name="month">
|
||||
<MonthUI v-model="month" :disabled="disabled" />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="周" name="week">
|
||||
<WeekUI v-model="week" :day="day" :disabled="disabled" />
|
||||
</ElTabPane>
|
||||
<ElTabPane v-if="!hideYear && !hideSecond" label="年" name="year">
|
||||
<YearUI v-model="year" :disabled="disabled" />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
<!-- 执行时间预览 -->
|
||||
<div class="time-list-container">
|
||||
<div class="time-inputs">
|
||||
<ElInput
|
||||
v-model="inputValues.minute"
|
||||
@blur="onInputBlur"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="label-text" @click="activeKey = 'minute'">分</span>
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElInput
|
||||
v-model="inputValues.hour"
|
||||
@blur="onInputBlur"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="label-text" @click="activeKey = 'hour'">时</span>
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElInput
|
||||
v-model="inputValues.day"
|
||||
@blur="onInputBlur"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="label-text" @click="activeKey = 'day'">日</span>
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElInput
|
||||
v-model="inputValues.month"
|
||||
@blur="onInputBlur"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="label-text" @click="activeKey = 'month'">月</span>
|
||||
</template>
|
||||
</ElInput>
|
||||
<ElInput
|
||||
v-model="inputValues.week"
|
||||
@blur="onInputBlur"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #prepend>
|
||||
<span class="label-text" @click="activeKey = 'week'">周</span>
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
|
||||
<ElInput
|
||||
v-model="inputValues.cron"
|
||||
@blur="onInputCronBlur"
|
||||
:disabled="disabled"
|
||||
class="cron-expression-input"
|
||||
>
|
||||
<template #prepend>
|
||||
<ElTooltip title="Cron表达式">Cron表达式</ElTooltip>
|
||||
</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
|
||||
<div class="preview-container">
|
||||
<div class="preview-label">近十次执行时间(不含年)</div>
|
||||
<ElInput
|
||||
v-model="preTimeList"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
readonly
|
||||
class="preview-textarea"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-inner {
|
||||
padding: 0 16px 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.time-list-container {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.time-inputs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
padding: 0 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.label-text:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.cron-expression-input {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.preview-textarea {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ZqDialog } from '#/components/zq-dialog';
|
||||
|
||||
import CronInner from './cron-inner.vue';
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
disabled?: boolean;
|
||||
hideSecond?: boolean;
|
||||
hideYear?: boolean;
|
||||
remote?: (
|
||||
cron: string,
|
||||
timestamp: number,
|
||||
callback: (result: string) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
disabled: false,
|
||||
hideSecond: true,
|
||||
hideYear: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'ok']);
|
||||
|
||||
const visible = ref(false);
|
||||
const innerValue = ref(props.modelValue);
|
||||
|
||||
const attrs = computed(() => ({
|
||||
modelValue: innerValue.value,
|
||||
disabled: props.disabled,
|
||||
hideSecond: props.hideSecond,
|
||||
hideYear: props.hideYear,
|
||||
remote: props.remote,
|
||||
}));
|
||||
|
||||
function openModal() {
|
||||
visible.value = true;
|
||||
innerValue.value = props.modelValue;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
emit('update:modelValue', innerValue.value);
|
||||
handleCancel();
|
||||
emit('ok');
|
||||
}
|
||||
|
||||
function handleCronChange(value: string) {
|
||||
innerValue.value = value;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openModal,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZqDialog
|
||||
v-model="visible"
|
||||
title="Cron表达式"
|
||||
width="50%"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<CronInner v-bind="attrs" @change="handleCronChange" />
|
||||
</ZqDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CronSelectorEmits, CronSelectorProps } from './types';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { EditOutlined } from '@vben/icons';
|
||||
|
||||
import { ElButton, ElInput } from 'element-plus';
|
||||
|
||||
import CronModal from './cron-modal.vue';
|
||||
|
||||
interface Props extends CronSelectorProps {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
disabled: false,
|
||||
hideSecond: true,
|
||||
hideYear: true,
|
||||
placeholder: 'Cron表达式',
|
||||
});
|
||||
|
||||
const emit = defineEmits<CronSelectorEmits>();
|
||||
|
||||
const value = computed({
|
||||
get: () => props.modelValue || '',
|
||||
set: (val) => {
|
||||
emit('update:modelValue', val);
|
||||
},
|
||||
});
|
||||
|
||||
const editCronValue = ref(props.modelValue || '');
|
||||
const cronModalRef = ref();
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal !== editCronValue.value) {
|
||||
editCronValue.value = newVal || '';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function showConfigModal() {
|
||||
if (props.disabled) return;
|
||||
cronModalRef.value?.openModal();
|
||||
}
|
||||
|
||||
function handleCronModalUpdate(newValue: string) {
|
||||
editCronValue.value = newValue;
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
emit('change', editCronValue.value);
|
||||
emit('update:modelValue', editCronValue.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-selector">
|
||||
<ElInput
|
||||
v-model="value"
|
||||
:placeholder="placeholder"
|
||||
readonly
|
||||
:disabled="disabled"
|
||||
clearable
|
||||
>
|
||||
<template #suffix>
|
||||
<ElButton
|
||||
link
|
||||
:icon="EditOutlined"
|
||||
@click="showConfigModal"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</template>
|
||||
</ElInput>
|
||||
<CronModal
|
||||
ref="cronModalRef"
|
||||
v-model="editCronValue"
|
||||
:disabled="disabled"
|
||||
:hide-year="hideYear"
|
||||
:hide-second="hideSecond"
|
||||
:remote="remote"
|
||||
@update:model-value="handleCronModalUpdate"
|
||||
@ok="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-selector {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,256 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { ElButton, ElCard, ElTable, ElTableColumn } from 'element-plus';
|
||||
|
||||
import { CronSelector } from './index';
|
||||
|
||||
const basicCron = ref('0 20 * * *');
|
||||
const secondCron = ref('0 0 * * * * *');
|
||||
const precisionCron = ref('0 0 0 1 1 * 2024');
|
||||
const disabledCron = ref('0 0 * * *');
|
||||
|
||||
const cronExpressions = [
|
||||
{
|
||||
description: '每分钟',
|
||||
expression: '* * * * *',
|
||||
explanation: '在每一分钟的每一秒执行',
|
||||
},
|
||||
{
|
||||
description: '每小时',
|
||||
expression: '0 * * * *',
|
||||
explanation: '在每一小时的整点执行',
|
||||
},
|
||||
{
|
||||
description: '每天午夜',
|
||||
expression: '0 0 * * *',
|
||||
explanation: '每天凌晨 00:00 执行',
|
||||
},
|
||||
{
|
||||
description: '每周一 8 点',
|
||||
expression: '0 8 * * 1',
|
||||
explanation: '每周一上午 8:00 执行',
|
||||
},
|
||||
{
|
||||
description: '每月 1 号',
|
||||
expression: '0 0 1 * *',
|
||||
explanation: '每个月的第 1 天凌晨 00:00 执行',
|
||||
},
|
||||
{
|
||||
description: '工作日 9 点',
|
||||
expression: '0 9 * * 1-5',
|
||||
explanation: '周一到周五上午 9:00 执行',
|
||||
},
|
||||
{
|
||||
description: '每 15 分钟',
|
||||
expression: '0/15 * * * *',
|
||||
explanation: '从 00 分开始,每隔 15 分钟执行',
|
||||
},
|
||||
{
|
||||
description: '每两小时',
|
||||
expression: '0 0/2 * * *',
|
||||
explanation: '从 00 时开始,每隔 2 小时执行',
|
||||
},
|
||||
{
|
||||
description: '午餐时间',
|
||||
expression: '0 12,18 * * *',
|
||||
explanation: '每天中午 12 点和晚上 6 点执行',
|
||||
},
|
||||
{
|
||||
description: '范围',
|
||||
expression: '0 9-17 * * *',
|
||||
explanation: '每天 9 点到 17 点每小时执行',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-example">
|
||||
<h1>Cron 选择器示例</h1>
|
||||
|
||||
<!-- 基础用法 -->
|
||||
<ElCard class="example-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>基础用法</span>
|
||||
<div class="demo-code">基础配置,隐藏秒和年</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="demo-content">
|
||||
<CronSelector v-model="basicCron" placeholder="输入或编辑Cron表达式" />
|
||||
<div class="demo-result">
|
||||
<p><strong>当前表达式:</strong> {{ basicCron }}</p>
|
||||
<p><strong>说明:</strong> 每天晚上 8 点执行</p>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- 支持秒 -->
|
||||
<ElCard class="example-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>支持秒</span>
|
||||
<div class="demo-code">显示秒配置</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="demo-content">
|
||||
<CronSelector
|
||||
v-model="secondCron"
|
||||
:hide-second="false"
|
||||
placeholder="包含秒的Cron表达式"
|
||||
/>
|
||||
<div class="demo-result">
|
||||
<p><strong>当前表达式:</strong> {{ secondCron }}</p>
|
||||
<p><strong>说明:</strong> 每分钟的第 0 秒执行</p>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- 支持秒和年 -->
|
||||
<ElCard class="example-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>支持秒和年</span>
|
||||
<div class="demo-code">精确到年</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="demo-content">
|
||||
<CronSelector
|
||||
v-model="precisionCron"
|
||||
:hide-second="false"
|
||||
:hide-year="false"
|
||||
placeholder="最完整的Cron表达式"
|
||||
/>
|
||||
<div class="demo-result">
|
||||
<p><strong>当前表达式:</strong> {{ precisionCron }}</p>
|
||||
<p><strong>说明:</strong> 仅在 2024 年执行</p>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- 禁用状态 -->
|
||||
<ElCard class="example-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>禁用状态</span>
|
||||
<div class="demo-code">不可编辑</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="demo-content">
|
||||
<CronSelector
|
||||
v-model="disabledCron"
|
||||
:disabled="true"
|
||||
placeholder="已禁用"
|
||||
/>
|
||||
<div class="demo-result">
|
||||
<p><strong>当前表达式:</strong> {{ disabledCron }}</p>
|
||||
<p><strong>说明:</strong> 此组件已被禁用</p>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- 常见表达式示例 -->
|
||||
<ElCard class="example-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>快速选择</span>
|
||||
<div class="demo-code">常见的Cron表达式</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="demo-content">
|
||||
<div class="quick-select">
|
||||
<ElButton @click="basicCron = '* * * * *'">每分钟</ElButton>
|
||||
<ElButton @click="basicCron = '0 * * * *'">每小时</ElButton>
|
||||
<ElButton @click="basicCron = '0 0 * * *'">每天</ElButton>
|
||||
<ElButton @click="basicCron = '0 8 * * 1-5'">工作日 8 点</ElButton>
|
||||
<ElButton @click="basicCron = '0 0 1 * *'">每月 1 号</ElButton>
|
||||
<ElButton @click="basicCron = '0 0 * * 0'">每周日</ElButton>
|
||||
<ElButton @click="basicCron = '0/15 * * * *'">每 15 分钟</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<!-- Cron 表达式说明 -->
|
||||
<ElCard class="example-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Cron 表达式说明</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="demo-content">
|
||||
<ElTable :data="cronExpressions" stripe style="width: 100%">
|
||||
<ElTableColumn prop="description" label="描述" width="200" />
|
||||
<ElTableColumn prop="expression" label="表达式" width="200" />
|
||||
<ElTableColumn prop="explanation" label="解释" />
|
||||
<ElTableColumn label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElButton link type="primary" @click="basicCron = row.expression">
|
||||
使用
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-example {
|
||||
max-width: 1200px;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cron-example h1 {
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.example-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.demo-code {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.demo-result {
|
||||
padding: 12px;
|
||||
margin-top: 16px;
|
||||
background-color: #f5f7fa;
|
||||
border-left: 3px solid #409eff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.demo-result p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.demo-result strong {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.quick-select {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-button) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as CronInner } from './cron-inner.vue';
|
||||
export { default as CronModal } from './cron-modal.vue';
|
||||
export { default as CronSelector } from './cron-selector.vue';
|
||||
export type { CronSelectorEmits, CronSelectorProps } from './types';
|
||||
export { TypeEnum } from './types';
|
||||
@@ -0,0 +1,185 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
} from 'element-plus';
|
||||
|
||||
import { TypeEnum, useTabSetup } from './useTabMixin';
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
disabled?: boolean;
|
||||
week?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '*',
|
||||
disabled: false,
|
||||
week: '*',
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const disabledChoice = computed(() => {
|
||||
return (props.week && props.week !== '*') || props.disabled;
|
||||
});
|
||||
|
||||
const setup = useTabSetup(
|
||||
props,
|
||||
{ emit },
|
||||
{
|
||||
defaultValue: '*',
|
||||
valueWork: 1,
|
||||
minValue: 1,
|
||||
maxValue: 31,
|
||||
valueRange: { start: 1, end: 31 },
|
||||
valueLoop: { start: 1, interval: 1 },
|
||||
disabled: disabledChoice,
|
||||
},
|
||||
);
|
||||
|
||||
const typeWorkAttrs = computed(() => ({
|
||||
disabled:
|
||||
setup.type.value !== TypeEnum.work ||
|
||||
props.disabled ||
|
||||
disabledChoice.value,
|
||||
...setup.inputNumberAttrs.value,
|
||||
}));
|
||||
|
||||
watch(
|
||||
() => props.week,
|
||||
() => {
|
||||
setup.updateValue(disabledChoice.value ? '*' : setup.computeValue.value);
|
||||
},
|
||||
);
|
||||
|
||||
function handleTypeChange() {
|
||||
// 类型改变时的处理
|
||||
}
|
||||
|
||||
const {
|
||||
type,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueList,
|
||||
specifyRange,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
beforeRadioAttrs,
|
||||
} = setup;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-config-list">
|
||||
<div class="item tip-item">
|
||||
<span class="tip-info">日和周只能设置其中之一</span>
|
||||
</div>
|
||||
<ElRadioGroup v-model="type" @change="handleTypeChange">
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.every" :disabled="disabledChoice">
|
||||
每日
|
||||
</ElRadio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.range" :disabled="disabledChoice">
|
||||
区间
|
||||
</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.start"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 日 至 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.end"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 日 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.loop" :disabled="disabledChoice">
|
||||
循环
|
||||
</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.start"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 日开始,间隔 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.interval"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 日 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.last" :disabled="disabledChoice">
|
||||
最后一日
|
||||
</ElRadio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.specify" :disabled="disabledChoice">
|
||||
指定
|
||||
</ElRadio>
|
||||
<div class="checkbox-list">
|
||||
<ElCheckboxGroup v-model="valueList">
|
||||
<ElCheckbox
|
||||
v-for="i in specifyRange"
|
||||
:key="i"
|
||||
:label="i"
|
||||
:disabled="typeSpecifyAttrs.disabled"
|
||||
>
|
||||
{{ i }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-config-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tip-info {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
margin-top: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
} from 'element-plus';
|
||||
|
||||
import { TypeEnum, useTabProps, useTabSetup } from './useTabMixin';
|
||||
|
||||
const props = defineProps({
|
||||
...useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const {
|
||||
type,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueList,
|
||||
specifyRange,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
beforeRadioAttrs,
|
||||
} = useTabSetup(
|
||||
props,
|
||||
{ emit },
|
||||
{
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 23,
|
||||
valueRange: { start: 0, end: 23 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
},
|
||||
);
|
||||
|
||||
function handleTypeChange() {
|
||||
// 类型改变时的处理
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-config-list">
|
||||
<ElRadioGroup v-model="type" @change="handleTypeChange">
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.every" v-bind="beforeRadioAttrs">
|
||||
每时
|
||||
</ElRadio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.range" v-bind="beforeRadioAttrs">
|
||||
区间
|
||||
</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.start"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 时 至 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.end"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 时 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.start"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 时开始,间隔 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.interval"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 时 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">
|
||||
指定
|
||||
</ElRadio>
|
||||
<div class="checkbox-list">
|
||||
<ElCheckboxGroup v-model="valueList">
|
||||
<ElCheckbox
|
||||
v-for="i in specifyRange"
|
||||
:key="i"
|
||||
:label="i"
|
||||
:disabled="typeSpecifyAttrs.disabled"
|
||||
>
|
||||
{{ i }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-config-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
margin-top: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
} from 'element-plus';
|
||||
|
||||
import { TypeEnum, useTabProps, useTabSetup } from './useTabMixin';
|
||||
|
||||
const props = defineProps({
|
||||
...useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const {
|
||||
type,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueList,
|
||||
specifyRange,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
beforeRadioAttrs,
|
||||
} = useTabSetup(
|
||||
props,
|
||||
{ emit },
|
||||
{
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 59,
|
||||
valueRange: { start: 0, end: 59 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
},
|
||||
);
|
||||
|
||||
function handleTypeChange() {
|
||||
// 类型改变时的处理
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-config-list">
|
||||
<ElRadioGroup v-model="type" @change="handleTypeChange">
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.every" v-bind="beforeRadioAttrs">
|
||||
每分
|
||||
</ElRadio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.range" v-bind="beforeRadioAttrs">
|
||||
区间
|
||||
</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.start"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 分 至 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.end"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 分 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.start"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 分开始,间隔 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.interval"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 分 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">
|
||||
指定
|
||||
</ElRadio>
|
||||
<div class="checkbox-list">
|
||||
<ElCheckboxGroup v-model="valueList">
|
||||
<ElCheckbox
|
||||
v-for="i in specifyRange"
|
||||
:key="i"
|
||||
:label="i"
|
||||
:value="i"
|
||||
:disabled="typeSpecifyAttrs.disabled"
|
||||
/>
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-config-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
} from 'element-plus';
|
||||
|
||||
import { TypeEnum, useTabProps, useTabSetup } from './useTabMixin';
|
||||
|
||||
const props = defineProps({
|
||||
...useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const {
|
||||
type,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueList,
|
||||
specifyRange,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
beforeRadioAttrs,
|
||||
} = useTabSetup(
|
||||
props,
|
||||
{ emit },
|
||||
{
|
||||
defaultValue: '*',
|
||||
minValue: 1,
|
||||
maxValue: 12,
|
||||
valueRange: { start: 1, end: 12 },
|
||||
valueLoop: { start: 1, interval: 1 },
|
||||
},
|
||||
);
|
||||
|
||||
function handleTypeChange() {
|
||||
// 类型改变时的处理
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-config-list">
|
||||
<ElRadioGroup v-model="type" @change="handleTypeChange">
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.every" v-bind="beforeRadioAttrs">
|
||||
每月
|
||||
</ElRadio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.range" v-bind="beforeRadioAttrs">
|
||||
区间
|
||||
</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.start"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 月 至 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.end"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 月 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.start"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 月开始,间隔 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.interval"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 月 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">
|
||||
指定
|
||||
</ElRadio>
|
||||
<div class="checkbox-list">
|
||||
<ElCheckboxGroup v-model="valueList">
|
||||
<ElCheckbox
|
||||
v-for="i in specifyRange"
|
||||
:key="i"
|
||||
:label="i"
|
||||
:disabled="typeSpecifyAttrs.disabled"
|
||||
>
|
||||
{{ i }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-config-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
margin-top: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
} from 'element-plus';
|
||||
|
||||
import { TypeEnum, useTabProps, useTabSetup } from './useTabMixin';
|
||||
|
||||
const props = defineProps({
|
||||
...useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const {
|
||||
type,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueList,
|
||||
specifyRange,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
beforeRadioAttrs,
|
||||
} = useTabSetup(
|
||||
props,
|
||||
{ emit },
|
||||
{
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 59,
|
||||
valueRange: { start: 0, end: 59 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
},
|
||||
);
|
||||
|
||||
function handleTypeChange() {
|
||||
// 类型改变时的处理
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-config-list">
|
||||
<ElRadioGroup v-model="type" @change="handleTypeChange">
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.every" v-bind="beforeRadioAttrs">
|
||||
每秒
|
||||
</ElRadio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.range" v-bind="beforeRadioAttrs">
|
||||
区间
|
||||
</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.start"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 秒 至 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.end"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 秒 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.start"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 秒开始,间隔 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.interval"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 秒 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">
|
||||
指定
|
||||
</ElRadio>
|
||||
<div class="checkbox-list">
|
||||
<ElCheckboxGroup v-model="valueList">
|
||||
<ElCheckbox
|
||||
v-for="i in specifyRange"
|
||||
:key="i"
|
||||
:label="i"
|
||||
:disabled="typeSpecifyAttrs.disabled"
|
||||
>
|
||||
{{ i }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-config-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
margin-top: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
} from 'element-plus';
|
||||
|
||||
import { TypeEnum, useTabSetup } from './useTabMixin';
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
disabled?: boolean;
|
||||
day?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '*',
|
||||
disabled: false,
|
||||
day: '*',
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const weekLabels = {
|
||||
0: '周日',
|
||||
1: '周一',
|
||||
2: '周二',
|
||||
3: '周三',
|
||||
4: '周四',
|
||||
5: '周五',
|
||||
6: '周六',
|
||||
};
|
||||
|
||||
const disabledChoice = computed(() => {
|
||||
return (props.day && props.day !== '*') || props.disabled;
|
||||
});
|
||||
|
||||
const setup = useTabSetup(
|
||||
props,
|
||||
{ emit },
|
||||
{
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 6,
|
||||
valueRange: { start: 0, end: 6 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
disabled: disabledChoice,
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.day,
|
||||
() => {
|
||||
setup.updateValue(disabledChoice.value ? '*' : setup.computeValue.value);
|
||||
},
|
||||
);
|
||||
|
||||
function handleTypeChange() {
|
||||
// 类型改变时的处理
|
||||
}
|
||||
|
||||
const {
|
||||
type,
|
||||
valueRange,
|
||||
valueList,
|
||||
specifyRange,
|
||||
typeRangeAttrs,
|
||||
typeSpecifyAttrs,
|
||||
beforeRadioAttrs,
|
||||
} = setup;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-config-list">
|
||||
<div class="item tip-item">
|
||||
<span class="tip-info">日和周只能设置其中之一</span>
|
||||
</div>
|
||||
<ElRadioGroup v-model="type" @change="handleTypeChange">
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.every" :disabled="disabledChoice">
|
||||
每周
|
||||
</ElRadio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.range" :disabled="disabledChoice">
|
||||
区间
|
||||
</ElRadio>
|
||||
<span class="label"> 从周 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.start"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 至周 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.end"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.specify" :disabled="disabledChoice">
|
||||
指定
|
||||
</ElRadio>
|
||||
<div class="checkbox-list">
|
||||
<ElCheckboxGroup v-model="valueList">
|
||||
<ElCheckbox
|
||||
v-for="(label, value) in weekLabels"
|
||||
:key="value"
|
||||
:label="value"
|
||||
:disabled="typeSpecifyAttrs.disabled"
|
||||
>
|
||||
{{ label }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-config-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tip-info {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
margin-top: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElInputNumber,
|
||||
ElRadio,
|
||||
ElRadioGroup,
|
||||
} from 'element-plus';
|
||||
|
||||
import { TypeEnum, useTabProps, useTabSetup } from './useTabMixin';
|
||||
|
||||
const props = defineProps({
|
||||
...useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const currentYear = new Date().getFullYear();
|
||||
const {
|
||||
type,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueList,
|
||||
specifyRange,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
beforeRadioAttrs,
|
||||
} = useTabSetup(
|
||||
props,
|
||||
{ emit },
|
||||
{
|
||||
defaultValue: '*',
|
||||
minValue: currentYear,
|
||||
maxValue: currentYear + 10,
|
||||
valueRange: { start: currentYear, end: currentYear + 10 },
|
||||
valueLoop: { start: currentYear, interval: 1 },
|
||||
},
|
||||
);
|
||||
|
||||
function handleTypeChange() {
|
||||
// 类型改变时的处理
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-config-list">
|
||||
<ElRadioGroup v-model="type" @change="handleTypeChange">
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.every" v-bind="beforeRadioAttrs">
|
||||
每年
|
||||
</ElRadio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.range" v-bind="beforeRadioAttrs">
|
||||
区间
|
||||
</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.start"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 年 至 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueRange.end"
|
||||
v-bind="typeRangeAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 年 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</ElRadio>
|
||||
<span class="label"> 从 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.start"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 年开始,间隔 </span>
|
||||
<ElInputNumber
|
||||
v-model="valueLoop.interval"
|
||||
v-bind="typeLoopAttrs"
|
||||
:step="1"
|
||||
/>
|
||||
<span class="label"> 年 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<ElRadio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">
|
||||
指定
|
||||
</ElRadio>
|
||||
<div class="year-specify">
|
||||
<ElCheckboxGroup v-model="valueList">
|
||||
<ElCheckbox
|
||||
v-for="i in specifyRange"
|
||||
:key="i"
|
||||
:label="i"
|
||||
:disabled="typeSpecifyAttrs.disabled"
|
||||
>
|
||||
{{ i }}
|
||||
</ElCheckbox>
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
</div>
|
||||
</ElRadioGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="css">
|
||||
.cron-config-list {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.year-specify {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
margin-top: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 主要用于日和星期的互斥使用
|
||||
*/
|
||||
import { computed, inject, reactive, ref, unref, watch } from 'vue';
|
||||
|
||||
export enum TypeEnum {
|
||||
every = 'EVERY',
|
||||
last = 'LAST',
|
||||
loop = 'LOOP',
|
||||
range = 'RANGE',
|
||||
specify = 'SPECIFY',
|
||||
unset = 'UNSET',
|
||||
work = 'WORK',
|
||||
}
|
||||
|
||||
export interface UseTabOptions {
|
||||
defaultValue?: string;
|
||||
defaultType?: TypeEnum;
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
valueRange: { end: number; start: number };
|
||||
valueLoop: { interval: number; start: number };
|
||||
valueWeek?: Record<string, any>;
|
||||
valueWork?: number;
|
||||
disabled?: (() => boolean) | boolean;
|
||||
}
|
||||
|
||||
export interface UseTabProps {
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共 props
|
||||
*/
|
||||
export function useTabProps(options?: Partial<UseTabOptions>) {
|
||||
const defaultValue = options?.defaultValue ?? '?';
|
||||
return {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: defaultValue,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
...options?.defaultValue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 公共 setup
|
||||
*/
|
||||
export function useTabSetup(props: any, context: any, options: UseTabOptions) {
|
||||
const { emit } = context;
|
||||
const prefixCls = inject('prefixCls', 'cron');
|
||||
const defaultValue = ref(options?.defaultValue ?? '?');
|
||||
|
||||
// 类型
|
||||
const type = ref(options.defaultType ?? TypeEnum.every);
|
||||
const valueList = ref<any[]>([]);
|
||||
|
||||
// 对于不同的类型,所定义的值也有所不同
|
||||
const valueRange = reactive(options.valueRange);
|
||||
const valueLoop = reactive(options.valueLoop);
|
||||
const valueWeek = reactive(options.valueWeek || {});
|
||||
const valueWork = ref(options.valueWork);
|
||||
const maxValue = ref(options.maxValue);
|
||||
const minValue = ref(options.minValue);
|
||||
|
||||
// 根据不同的类型计算出的value
|
||||
const computeValue = computed(() => {
|
||||
const valueArray: any[] = [];
|
||||
switch (type.value) {
|
||||
case TypeEnum.every: {
|
||||
valueArray.push('*');
|
||||
break;
|
||||
}
|
||||
case TypeEnum.last: {
|
||||
valueArray.push('L');
|
||||
break;
|
||||
}
|
||||
case TypeEnum.loop: {
|
||||
valueArray.push(`${valueLoop.start}/${valueLoop.interval}`);
|
||||
break;
|
||||
}
|
||||
case TypeEnum.range: {
|
||||
valueArray.push(`${valueRange.start}-${valueRange.end}`);
|
||||
break;
|
||||
}
|
||||
case TypeEnum.specify: {
|
||||
if (valueList.value.length === 0) {
|
||||
valueList.value.push(minValue.value);
|
||||
}
|
||||
valueArray.push(valueList.value.join(','));
|
||||
break;
|
||||
}
|
||||
case TypeEnum.unset: {
|
||||
valueArray.push('?');
|
||||
break;
|
||||
}
|
||||
case TypeEnum.work: {
|
||||
valueArray.push(`${valueWork.value}W`);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
valueArray.push(defaultValue.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return valueArray.length > 0 ? valueArray.join('') : defaultValue.value;
|
||||
});
|
||||
|
||||
// 指定值范围区间,介于最小值和最大值之间
|
||||
const specifyRange = computed(() => {
|
||||
const range: number[] = [];
|
||||
if (maxValue.value != null) {
|
||||
for (let i = minValue.value; i <= maxValue.value; i++) {
|
||||
range.push(i);
|
||||
}
|
||||
}
|
||||
return range;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val !== computeValue.value) {
|
||||
parseValue(val);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(computeValue, (v) => updateValue(v));
|
||||
|
||||
function updateValue(value: string) {
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* parseValue
|
||||
*/
|
||||
function parseValue(value: string) {
|
||||
if (value === computeValue.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!value || value === defaultValue.value) {
|
||||
type.value = TypeEnum.every;
|
||||
} else if (value.includes('?')) {
|
||||
type.value = TypeEnum.unset;
|
||||
} else if (value.includes('-')) {
|
||||
type.value = TypeEnum.range;
|
||||
const values = value.split('-');
|
||||
if (values.length >= 2) {
|
||||
valueRange.start = Number.parseInt(values[0]);
|
||||
valueRange.end = Number.parseInt(values[1]);
|
||||
}
|
||||
} else if (value.includes('/')) {
|
||||
type.value = TypeEnum.loop;
|
||||
const values = value.split('/');
|
||||
if (values.length >= 2) {
|
||||
valueLoop.start = value[0] === '*' ? 0 : Number.parseInt(values[0]);
|
||||
valueLoop.interval = Number.parseInt(values[1]);
|
||||
}
|
||||
} else if (value.includes('W')) {
|
||||
type.value = TypeEnum.work;
|
||||
const values = value.split('W');
|
||||
if (!values[0] && !isNaN(Number.parseInt(values[0]))) {
|
||||
valueWork.value = Number.parseInt(values[0]);
|
||||
}
|
||||
} else if (value.includes('L')) {
|
||||
type.value = TypeEnum.last;
|
||||
} else if (value.includes(',') || !isNaN(Number.parseInt(value))) {
|
||||
type.value = TypeEnum.specify;
|
||||
valueList.value = value.split(',').map((item) => Number.parseInt(item));
|
||||
} else {
|
||||
type.value = TypeEnum.every;
|
||||
}
|
||||
} catch {
|
||||
type.value = TypeEnum.every;
|
||||
}
|
||||
}
|
||||
|
||||
const beforeRadioAttrs = computed(() => ({
|
||||
disabled: props.disabled || unref(options.disabled),
|
||||
}));
|
||||
|
||||
const inputNumberAttrs = computed(() => ({
|
||||
max: maxValue.value,
|
||||
min: minValue.value,
|
||||
}));
|
||||
|
||||
const typeRangeAttrs = computed(() => ({
|
||||
disabled:
|
||||
type.value !== TypeEnum.range ||
|
||||
props.disabled ||
|
||||
unref(options.disabled),
|
||||
...inputNumberAttrs.value,
|
||||
}));
|
||||
|
||||
const typeLoopAttrs = computed(() => ({
|
||||
disabled:
|
||||
type.value !== TypeEnum.loop || props.disabled || unref(options.disabled),
|
||||
...inputNumberAttrs.value,
|
||||
}));
|
||||
|
||||
const typeSpecifyAttrs = computed(() => ({
|
||||
disabled:
|
||||
type.value !== TypeEnum.specify ||
|
||||
props.disabled ||
|
||||
unref(options.disabled),
|
||||
}));
|
||||
|
||||
return {
|
||||
type,
|
||||
TypeEnum,
|
||||
prefixCls,
|
||||
defaultValue,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueWeek,
|
||||
valueList,
|
||||
valueWork,
|
||||
maxValue,
|
||||
minValue,
|
||||
computeValue,
|
||||
specifyRange,
|
||||
updateValue,
|
||||
parseValue,
|
||||
beforeRadioAttrs,
|
||||
inputNumberAttrs,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Cron Selector Props and Types
|
||||
*/
|
||||
export interface CronSelectorProps {
|
||||
/**
|
||||
* Cron 表达式
|
||||
*/
|
||||
modelValue?: string;
|
||||
|
||||
/**
|
||||
* 是否禁用
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* 是否隐藏秒
|
||||
* @default true
|
||||
*/
|
||||
hideSecond?: boolean;
|
||||
|
||||
/**
|
||||
* 是否隐藏年
|
||||
* @default true
|
||||
*/
|
||||
hideYear?: boolean;
|
||||
|
||||
/**
|
||||
* 占位符文本
|
||||
* @default 'Cron表达式'
|
||||
*/
|
||||
placeholder?: string;
|
||||
|
||||
/**
|
||||
* 远程获取执行时间列表的函数
|
||||
*/
|
||||
remote?: (
|
||||
cron: string,
|
||||
timestamp: number,
|
||||
callback: (result: string) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface CronSelectorEmits {
|
||||
/**
|
||||
* 当 Cron 表达式变化时触发
|
||||
*/
|
||||
'update:modelValue': [value: string];
|
||||
|
||||
/**
|
||||
* 当 Cron 表达式变化时触发
|
||||
*/
|
||||
change: [value: string];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab 类型枚举
|
||||
*/
|
||||
export enum TypeEnum {
|
||||
every = 'EVERY',
|
||||
last = 'LAST',
|
||||
loop = 'LOOP',
|
||||
range = 'RANGE',
|
||||
specify = 'SPECIFY',
|
||||
unset = 'UNSET',
|
||||
work = 'WORK',
|
||||
}
|
||||
Reference in New Issue
Block a user