Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -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',
}