feat: 新增cron组件
This commit is contained in:
parent
260058da45
commit
1f5d963557
@ -44,6 +44,7 @@
|
||||
"@iconify/vue": "4.1.1",
|
||||
"@sa/axios": "workspace:*",
|
||||
"@sa/color-palette": "workspace:*",
|
||||
"@sa/cron-input": "workspace:*",
|
||||
"@sa/hooks": "workspace:*",
|
||||
"@sa/materials": "workspace:*",
|
||||
"@sa/utils": "workspace:*",
|
||||
|
15
packages/cron-input/package.json
Normal file
15
packages/cron-input/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@sa/cron-input",
|
||||
"version": "1.0.3",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.9.0"
|
||||
}
|
||||
}
|
206
packages/cron-input/src/components/cron.vue
Normal file
206
packages/cron-input/src/components/cron.vue
Normal file
@ -0,0 +1,206 @@
|
||||
<script setup lang="ts">
|
||||
import cronParser from 'cron-parser';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { DEFAULT_CRON_EXPRESSION, DEFAULT_LOCALE, FIELDS, LOCALE_CN, TYPE } from '../shared/constants';
|
||||
import { weekNumberToLetter, zerofill } from '../shared/utils';
|
||||
import Locales from '../i18n';
|
||||
import CronBase from './internal/cron-base.vue';
|
||||
|
||||
defineOptions({ name: 'CronInput' });
|
||||
|
||||
interface Props {
|
||||
modelValue?: string;
|
||||
lang?: I18n.LocaleType;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: DEFAULT_CRON_EXPRESSION,
|
||||
lang: (JSON.parse(window.localStorage.getItem('lang')!) as I18n.LocaleType) || DEFAULT_LOCALE
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const [second, minute, hour, date, month, week, year = ''] = props.modelValue.split(' ');
|
||||
const cron = ref({ second, minute, hour, date, month, week, year });
|
||||
const activeKey = ref(FIELDS[0].value);
|
||||
const previewTime = ref(5);
|
||||
|
||||
const width = computed(() => {
|
||||
return props.lang === LOCALE_CN ? '460px' : '520px';
|
||||
});
|
||||
|
||||
const fields = computed(() => {
|
||||
return FIELDS.map(field => {
|
||||
const label = Locales[props.lang].field[field.value];
|
||||
return { ...field, label };
|
||||
});
|
||||
});
|
||||
|
||||
const expressionLabel = computed(() => {
|
||||
return Locales[props.lang].expression;
|
||||
});
|
||||
|
||||
const previewLabel = computed(() => {
|
||||
return Locales[props.lang].preview.join(previewTime.value.toString());
|
||||
});
|
||||
|
||||
const expression = computed(() => {
|
||||
return Object.values(cron.value).join(' ');
|
||||
});
|
||||
|
||||
const previews = computed(() => {
|
||||
let previewList = [];
|
||||
|
||||
try {
|
||||
const interval = cronParser.parseExpression(expression.value);
|
||||
|
||||
for (let i = 0; i < previewTime.value; i += 1) {
|
||||
const datetime = interval.next();
|
||||
const previewYear = zerofill(datetime.getFullYear());
|
||||
const previewMonth = zerofill(datetime.getMonth() + 1);
|
||||
const previewDate = zerofill(datetime.getDate());
|
||||
const previewHour = zerofill(datetime.getHours());
|
||||
const previewMinute = zerofill(datetime.getMinutes());
|
||||
const previewSecond = zerofill(datetime.getSeconds());
|
||||
|
||||
previewList.push(
|
||||
`${previewYear}-${previewMonth}-${previewDate} ${previewHour}:${previewMinute}:${previewSecond}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
previewList = ['此表达式暂时无法解析!'];
|
||||
}
|
||||
|
||||
return previewList;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => cron.value,
|
||||
val => {
|
||||
val.week = weekNumberToLetter(val.week);
|
||||
emit('update:modelValue', Object.values(val).join(' '));
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => cron.value.date,
|
||||
val => {
|
||||
if (val === TYPE.UNSPECIFIC) {
|
||||
if (cron.value.week === TYPE.UNSPECIFIC) {
|
||||
cron.value.week = TYPE.EVERY;
|
||||
}
|
||||
} else if (cron.value.week !== TYPE.UNSPECIFIC) {
|
||||
cron.value.week = TYPE.UNSPECIFIC;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => cron.value.week,
|
||||
val => {
|
||||
if (val === TYPE.UNSPECIFIC) {
|
||||
if (cron.value.date === TYPE.UNSPECIFIC) {
|
||||
cron.value.date = TYPE.EVERY;
|
||||
}
|
||||
} else if (cron.value.date !== TYPE.UNSPECIFIC) {
|
||||
cron.value.date = TYPE.UNSPECIFIC;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cron-wrapper" :style="{ maxWidth: width }">
|
||||
<NTabs v-model:value="activeKey" class="cron-tabs" type="segment">
|
||||
<NTabPane v-for="field in fields" :key="field.value" :name="field.value" :tab="field.label">
|
||||
<CronBase v-model="cron[field.value]" class="cron-base" :field="field" :locale="lang" />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
<div class="expression">
|
||||
<div class="title">
|
||||
<span class="label">{{ expressionLabel }}</span>
|
||||
</div>
|
||||
<div class="h-9px"></div>
|
||||
<span class="content">{{ expression }}</span>
|
||||
</div>
|
||||
<div class="preview">
|
||||
<div class="title">
|
||||
<span class="label">{{ previewLabel }}</span>
|
||||
</div>
|
||||
<div class="h-9px"></div>
|
||||
<ul class="list">
|
||||
<li v-for="(preview, index) in previews" :key="preview">
|
||||
<span class="index">{{ index + 1 }}</span>
|
||||
<span>{{ preview }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cron-wrapper {
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
.cron-tabs {
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 3px;
|
||||
|
||||
.cron-base {
|
||||
padding: 0 16px 16px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.expression,
|
||||
.preview {
|
||||
margin: 32px 0;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 3px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: -28px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 16px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-bottom: 0;
|
||||
|
||||
li:not(:first-child) {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.index {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background-color: rgb(var(--primary-color));
|
||||
color: #fff;
|
||||
margin-right: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
247
packages/cron-input/src/components/internal/cron-base.vue
Normal file
247
packages/cron-input/src/components/internal/cron-base.vue
Normal file
@ -0,0 +1,247 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { DATE, DEFAULT_LOCALE, LOCALE_EN, TYPE, WEEK, YEAR } from '../../shared/constants';
|
||||
import { generateSpecifies, weekLetterToNumber } from '../../shared/utils';
|
||||
import Locales from '../../i18n';
|
||||
import InputNumber from './input-number.vue';
|
||||
|
||||
defineOptions({ name: 'CronBase' });
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
field: {
|
||||
value: I18n.FieldType;
|
||||
label: string;
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
locale: I18n.LocaleType;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
locale: DEFAULT_LOCALE
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const labels = props.field.value === 'week' ? Object.values(Locales[props.locale].week) : null;
|
||||
|
||||
const { min, max, value: fieldValue } = props.field;
|
||||
|
||||
const type = ref(TYPE.EVERY);
|
||||
const range = ref([min, min + 1]);
|
||||
const step = ref([min, 1]);
|
||||
const well = ref([min, 1]);
|
||||
const specify = ref<number[]>([]);
|
||||
const weekday = ref(1);
|
||||
const lastDayOfWeek = ref(0);
|
||||
const rangeStart = ref<[number, number]>([min, max - 1]);
|
||||
const stepLeft = ref<[number, number]>([min, max]);
|
||||
const stepRight = ref<[number, number]>([1, max]);
|
||||
const wellLeft = ref<[number, number]>([0, 0]);
|
||||
const wellRight = ref<[number, number]>([0, 0]);
|
||||
const specifies = ref(generateSpecifies(min, max, labels));
|
||||
|
||||
if (fieldValue === WEEK) {
|
||||
wellLeft.value = [1, 5];
|
||||
wellRight.value = [min, max];
|
||||
}
|
||||
|
||||
const label = computed(() => {
|
||||
const i18n = Locales[props.locale];
|
||||
const { type: typeLabel, fieldAlias: fieldAliasLabel } = i18n;
|
||||
return {
|
||||
empty: typeLabel.empty,
|
||||
every: `${typeLabel.every}${fieldAliasLabel[props.field.value]}`,
|
||||
unspecific: typeLabel.unspecific,
|
||||
range: [
|
||||
typeLabel.range[0],
|
||||
(props.field.value === WEEK || props.locale === LOCALE_EN ? '' : props.field.label) + typeLabel.range[1],
|
||||
props.field.value === WEEK || props.locale === LOCALE_EN ? '' : props.field.label
|
||||
],
|
||||
step: [
|
||||
typeLabel.step[0],
|
||||
props.field.label + typeLabel.step[1],
|
||||
fieldAliasLabel[props.field.value] + typeLabel.step[2]
|
||||
],
|
||||
well: typeLabel.well,
|
||||
weekday: typeLabel.weekday,
|
||||
lastWeekday: typeLabel.lastWeekday,
|
||||
lastDayOfDate: typeLabel.lastDayOfDate,
|
||||
lastDayOfWeek: typeLabel.lastDayOfWeek,
|
||||
specify: typeLabel.specify
|
||||
};
|
||||
});
|
||||
|
||||
const isEnWeek = computed(() => props.field.value === WEEK && props.locale === LOCALE_EN);
|
||||
const rangeEnd = computed<[number, number]>(() => [range.value[0] + 1, props.field.max]);
|
||||
const isEmpty = computed(() => props.field.value === YEAR);
|
||||
const isUnspecific = computed(() => [DATE, WEEK].includes(props.field.value));
|
||||
const isStep = computed(() => props.field.value !== WEEK);
|
||||
const isWell = computed(() => props.field.value === WEEK);
|
||||
const isLastDayOfDate = computed(() => props.field.value === DATE);
|
||||
const isLastDayOfWeek = computed(() => props.field.value === WEEK);
|
||||
const isWeekday = computed(() => props.field.value === DATE);
|
||||
const isLastWeekday = computed(() => props.field.value === DATE);
|
||||
const value = computed(() => {
|
||||
switch (type.value) {
|
||||
case TYPE.EMPTY:
|
||||
case TYPE.UNSPECIFIC:
|
||||
case TYPE.LAST_WEEKDAY:
|
||||
case TYPE.EVERY:
|
||||
return type.value;
|
||||
case TYPE.RANGE:
|
||||
return range.value.join(type.value);
|
||||
case TYPE.STEP:
|
||||
return step.value.join(type.value);
|
||||
case TYPE.WELL:
|
||||
return well.value.join(type.value);
|
||||
case TYPE.WEEKDAY:
|
||||
return `${weekday.value}${type.value}`;
|
||||
case TYPE.LAST_DAY:
|
||||
return props.field.value === DATE ? type.value : `${lastDayOfWeek.value}${type.value}`;
|
||||
case TYPE.SPECIFY: {
|
||||
const specifyValue = specify.value;
|
||||
return specifyValue.length ? specifyValue.sort((a, b) => a - b).join(type.value) : `${specifyValue[0]}`;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
val => {
|
||||
let data = val;
|
||||
|
||||
if (props.field.value === WEEK) {
|
||||
data = weekLetterToNumber(val).replaceAll('7', '0');
|
||||
}
|
||||
|
||||
if ([TYPE.EMPTY, TYPE.UNSPECIFIC, TYPE.LAST_DAY, TYPE.LAST_WEEKDAY, TYPE.EVERY].includes(data)) {
|
||||
type.value = data;
|
||||
} else if (data.includes(TYPE.RANGE)) {
|
||||
type.value = TYPE.RANGE;
|
||||
range.value = data.split(TYPE.RANGE).map(i => Number.parseInt(i, 10));
|
||||
} else if (data.includes(TYPE.STEP)) {
|
||||
type.value = TYPE.STEP;
|
||||
step.value = data.split(TYPE.STEP).map(i => Number.parseInt(i, 10));
|
||||
} else if (data.includes(TYPE.WELL)) {
|
||||
type.value = TYPE.WELL;
|
||||
well.value = data.split(TYPE.WELL).map(i => Number.parseInt(i, 10));
|
||||
} else if (data.includes(TYPE.WEEKDAY)) {
|
||||
type.value = TYPE.WEEKDAY;
|
||||
weekday.value = Number.parseInt(data, 10);
|
||||
} else if (data.includes(TYPE.LAST_DAY)) {
|
||||
type.value = TYPE.LAST_DAY;
|
||||
lastDayOfWeek.value = Number.parseInt(data, 10);
|
||||
} else {
|
||||
type.value = TYPE.SPECIFY;
|
||||
specify.value = data.split(TYPE.SPECIFY).map(i => Number.parseInt(i, 10));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => value.value,
|
||||
val => {
|
||||
emit('update:modelValue', val);
|
||||
}
|
||||
);
|
||||
|
||||
const onRangeStartChange = (val: number) => {
|
||||
const [, ranEnd] = range.value;
|
||||
|
||||
if (val >= ranEnd) {
|
||||
range.value[1] = val + 1;
|
||||
}
|
||||
};
|
||||
|
||||
const onCheckboxGroupChange = (val: string[]) => {
|
||||
let checkType = TYPE.SPECIFY;
|
||||
|
||||
if (val.length === 0) {
|
||||
checkType = props.field.value === YEAR ? TYPE.EMPTY : TYPE.EVERY;
|
||||
}
|
||||
|
||||
type.value = checkType;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NRadioGroup v-model:value="type" class="flex-col">
|
||||
<NRadio v-if="isEmpty && field.value !== YEAR" class="cron-radio" :value="TYPE.EMPTY">{{ label.empty }}</NRadio>
|
||||
<NRadio class="cron-radio" :value="TYPE.EVERY">{{ label.every }}</NRadio>
|
||||
<NRadio v-if="isEmpty && field.value === YEAR" class="cron-radio" :value="TYPE.EMPTY">{{ label.empty }}</NRadio>
|
||||
<NRadio v-if="isUnspecific" class="cron-radio" :value="TYPE.UNSPECIFIC">{{ label.unspecific }}</NRadio>
|
||||
<div class="cron-radio flex items-center justify-start gap-5px">
|
||||
<NRadio :value="TYPE.RANGE" />
|
||||
{{ label.range[0] }}
|
||||
<InputNumber
|
||||
v-model="range[0]"
|
||||
:range="rangeStart"
|
||||
:field-value="field.value"
|
||||
:locale="locale"
|
||||
@update:value="onRangeStartChange"
|
||||
/>
|
||||
{{ label.range[1] }}
|
||||
<InputNumber v-model="range[1]" :range="rangeEnd" :field-value="field.value" :locale="locale" />
|
||||
{{ label.range[2] }}
|
||||
</div>
|
||||
<div v-if="isStep" class="cron-radio flex items-center justify-start gap-5px">
|
||||
<NRadio :value="TYPE.STEP" />
|
||||
<span>{{ label.step[0] }}</span>
|
||||
<InputNumber v-model="step[0]" :range="stepLeft" />
|
||||
<span>{{ label.step[1] }}</span>
|
||||
<InputNumber v-model="step[1]" :range="stepRight" />
|
||||
<span>{{ label.step[2] }}</span>
|
||||
</div>
|
||||
<div v-if="isWell" class="cron-radio flex items-center justify-start gap-5px">
|
||||
<NRadio :value="TYPE.WELL" />
|
||||
{{ label.well[0] }}
|
||||
<InputNumber v-model="well[1]" :range="[...wellLeft]" />
|
||||
{{ label.well[1] }}
|
||||
<InputNumber v-model="well[0]" :range="[...wellRight]" :field-value="field.value" :locale="locale" />
|
||||
</div>
|
||||
<div v-if="isWeekday" class="cron-radio flex items-center justify-start gap-5px">
|
||||
<NRadio :value="TYPE.WEEKDAY" />
|
||||
{{ label.weekday[0] }}
|
||||
<InputNumber v-model="weekday" :range="rangeStart" />
|
||||
{{ label.weekday[1] }}
|
||||
</div>
|
||||
<NRadio v-if="isLastWeekday" class="cron-radio" :value="TYPE.LAST_WEEKDAY">{{ label.lastWeekday }}</NRadio>
|
||||
<NRadio v-if="isLastDayOfDate" class="cron-radio" :value="TYPE.LAST_DAY">{{ label.lastDayOfDate }}</NRadio>
|
||||
<div v-if="isLastDayOfWeek" class="cron-radio flex items-center justify-start gap-5px">
|
||||
<NRadio v-if="isLastDayOfWeek" :value="TYPE.LAST_DAY" />
|
||||
{{ label.lastDayOfWeek }}
|
||||
<InputNumber v-model="lastDayOfWeek" :range="[0, 6]" :field-value="field.value" :locale="locale" />
|
||||
</div>
|
||||
<div class="cron-radio flex flex-wrap items-center justify-start gap-5px">
|
||||
<NRadio class="cron-radio" :value="TYPE.SPECIFY">{{ label.specify }}</NRadio>
|
||||
<NCheckboxGroup
|
||||
v-model:checked="specify"
|
||||
class="p-l-22px"
|
||||
:class="{ 'checkbox-group-en-week': isEnWeek }"
|
||||
@update:checked="onCheckboxGroupChange"
|
||||
>
|
||||
<NCheckbox
|
||||
v-for="option in specifies"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
class="min-w-50px"
|
||||
/>
|
||||
</NCheckboxGroup>
|
||||
</div>
|
||||
</NRadioGroup>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cron-radio:not(:first-child) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
59
packages/cron-input/src/components/internal/input-number.vue
Normal file
59
packages/cron-input/src/components/internal/input-number.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { formatterWeek, parserWeek } from '../../shared/utils';
|
||||
import { WEEK } from '../../shared/constants';
|
||||
|
||||
defineOptions({ name: 'InputNumber' });
|
||||
|
||||
interface Props {
|
||||
modelValue: number;
|
||||
range: [number, number];
|
||||
fieldValue?: string;
|
||||
locale?: I18n.LocaleType;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: number): void;
|
||||
(e: 'change', value: number | null): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
emit('update:modelValue', val);
|
||||
}
|
||||
});
|
||||
|
||||
const formatter = (val: number) => {
|
||||
return props.fieldValue === WEEK ? formatterWeek(val.toString(), props.locale!) : null;
|
||||
};
|
||||
|
||||
const parser = (val: string) => {
|
||||
return props.fieldValue === WEEK ? parserWeek(val, props.locale!) : null;
|
||||
};
|
||||
|
||||
const onChange = (val: number | null): void => {
|
||||
emit('change', val);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NInputNumber
|
||||
v-model:value="value"
|
||||
:min="range[0]"
|
||||
:max="range[1]"
|
||||
class="w-90px"
|
||||
size="small"
|
||||
:formatter="formatter"
|
||||
:parser="parser"
|
||||
@update:value="onChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
53
packages/cron-input/src/i18n/i18n.d.ts
vendored
Normal file
53
packages/cron-input/src/i18n/i18n.d.ts
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
declare namespace I18n {
|
||||
type LocaleType = 'en-US' | 'zh-CN';
|
||||
|
||||
type FieldType = 'second' | 'minute' | 'hour' | 'date' | 'month' | 'week' | 'year';
|
||||
|
||||
interface LocaleFields {
|
||||
second: string;
|
||||
minute: string;
|
||||
hour: string;
|
||||
date: string;
|
||||
month: string;
|
||||
week: string;
|
||||
year: string;
|
||||
}
|
||||
|
||||
interface LocaleFieldAlias {
|
||||
second: string;
|
||||
minute: string;
|
||||
hour: string;
|
||||
date: string;
|
||||
month: string;
|
||||
week: string;
|
||||
year: string;
|
||||
}
|
||||
|
||||
interface LocaleTypes {
|
||||
empty: string;
|
||||
every: string;
|
||||
unspecific: string;
|
||||
range: string[];
|
||||
step: string[];
|
||||
well: string[];
|
||||
weekday: string[];
|
||||
lastWeekday: string;
|
||||
lastDayOfDate: string;
|
||||
lastDayOfWeek: string;
|
||||
specify: string;
|
||||
}
|
||||
|
||||
interface LocaleWeek {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface Translations {
|
||||
field: LocaleFields;
|
||||
fieldAlias: LocaleFieldAlias;
|
||||
type: LocaleTypes;
|
||||
week: LocaleWeek;
|
||||
expression: string;
|
||||
preview: string[];
|
||||
previewError: string;
|
||||
}
|
||||
}
|
10
packages/cron-input/src/i18n/index.ts
Normal file
10
packages/cron-input/src/i18n/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { LOCALE_CN, LOCALE_EN } from '../shared/constants';
|
||||
import zhCN from './langs/zh-cn';
|
||||
import enUS from './langs/en-us';
|
||||
|
||||
const locales: Record<string, I18n.Translations> = {
|
||||
[LOCALE_CN]: zhCN,
|
||||
[LOCALE_EN]: enUS
|
||||
};
|
||||
|
||||
export default locales;
|
47
packages/cron-input/src/i18n/langs/en-us.ts
Normal file
47
packages/cron-input/src/i18n/langs/en-us.ts
Normal file
@ -0,0 +1,47 @@
|
||||
const englishTranslations: I18n.Translations = {
|
||||
field: {
|
||||
second: 'Second',
|
||||
minute: 'Minute',
|
||||
hour: 'Hour',
|
||||
date: 'Date',
|
||||
month: 'Month',
|
||||
week: 'Week',
|
||||
year: 'Year'
|
||||
},
|
||||
fieldAlias: {
|
||||
second: 'second',
|
||||
minute: 'minute',
|
||||
hour: 'hour',
|
||||
date: 'date',
|
||||
month: 'month',
|
||||
week: 'week',
|
||||
year: 'year'
|
||||
},
|
||||
type: {
|
||||
empty: 'Empty',
|
||||
every: 'Every ',
|
||||
unspecific: 'Unspecific',
|
||||
range: ['From ', ' to ', ''],
|
||||
step: ['Start with ', ', execute every', ''],
|
||||
well: ['The ', ''],
|
||||
weekday: ['Nearest weekday to the ', ' of current month'],
|
||||
lastWeekday: 'Last weekday of current month',
|
||||
lastDayOfDate: 'Last day of current month',
|
||||
lastDayOfWeek: 'Last ',
|
||||
specify: 'Specify'
|
||||
},
|
||||
week: {
|
||||
Sunday: 'Sunday',
|
||||
Monday: 'Monday',
|
||||
Tuesday: 'Tuesday',
|
||||
Wednesday: 'Wednesday',
|
||||
Thursday: 'Thursday',
|
||||
Friday: 'Friday',
|
||||
Saturday: 'Saturday'
|
||||
},
|
||||
expression: 'The complete expression',
|
||||
preview: ['Last ', ' runtimes'],
|
||||
previewError: 'This expression is temporarily unparsed!'
|
||||
};
|
||||
|
||||
export default englishTranslations;
|
47
packages/cron-input/src/i18n/langs/zh-cn.ts
Normal file
47
packages/cron-input/src/i18n/langs/zh-cn.ts
Normal file
@ -0,0 +1,47 @@
|
||||
const chineseTranslations: I18n.Translations = {
|
||||
field: {
|
||||
second: '秒',
|
||||
minute: '分',
|
||||
hour: '时',
|
||||
date: '日',
|
||||
month: '月',
|
||||
week: '周',
|
||||
year: '年'
|
||||
},
|
||||
fieldAlias: {
|
||||
second: '秒钟',
|
||||
minute: '分钟',
|
||||
hour: '小时',
|
||||
date: '天',
|
||||
month: '个月',
|
||||
week: '星期',
|
||||
year: '年'
|
||||
},
|
||||
type: {
|
||||
empty: '不指定',
|
||||
every: '每',
|
||||
unspecific: '不指定',
|
||||
range: ['从', '到', ''],
|
||||
step: ['从', '开始,每', '执行一次'],
|
||||
well: ['当月第', '个'],
|
||||
weekday: ['离当月', '号最近的那个工作日'],
|
||||
lastWeekday: '当月最后一个工作日',
|
||||
lastDayOfDate: '当月最后一天',
|
||||
lastDayOfWeek: '当月最后一个',
|
||||
specify: '指定'
|
||||
},
|
||||
week: {
|
||||
Sunday: '星期日',
|
||||
Monday: '星期一',
|
||||
Tuesday: '星期二',
|
||||
Wednesday: '星期三',
|
||||
Thursday: '星期四',
|
||||
Friday: '星期五',
|
||||
Saturday: '星期六'
|
||||
},
|
||||
expression: '完整表达式',
|
||||
preview: ['最近', '次运行时间'],
|
||||
previewError: '此表达式暂时无法解析!'
|
||||
};
|
||||
|
||||
export default chineseTranslations;
|
3
packages/cron-input/src/index.ts
Normal file
3
packages/cron-input/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import Cron from './components/cron.vue';
|
||||
|
||||
export default Cron;
|
77
packages/cron-input/src/shared/constants.ts
Normal file
77
packages/cron-input/src/shared/constants.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export const MIN_SECOND = 0;
|
||||
export const MAX_SECOND = 59;
|
||||
|
||||
export const MIN_MINUTE = 0;
|
||||
export const MAX_MINUTE = 59;
|
||||
|
||||
export const MIN_HOUR = 0;
|
||||
export const MAX_HOUR = 23;
|
||||
|
||||
export const MIN_DATE = 1;
|
||||
export const MAX_DATE = 31;
|
||||
|
||||
export const MIN_MONTH = 1;
|
||||
export const MAX_MONTH = 12;
|
||||
|
||||
export const MIN_WEEK = 0;
|
||||
export const MAX_WEEK = 6;
|
||||
|
||||
export const MIN_YEAR = new Date().getFullYear();
|
||||
export const MAX_YEAR = 2099;
|
||||
|
||||
export const SUNDAY = 'sunday';
|
||||
export const MONDAY = 'monday';
|
||||
export const TUESDAY = 'tuesday';
|
||||
export const WEDNESDAY = 'wednesday';
|
||||
export const THURSDAY = 'thursday';
|
||||
export const FRIDAY = 'friday';
|
||||
export const SATURDAY = 'saturday';
|
||||
|
||||
export const WEEKS = [
|
||||
{ value: SUNDAY, abbr: 'SUN', index: '0' },
|
||||
{ value: MONDAY, abbr: 'MON', index: '1' },
|
||||
{ value: TUESDAY, abbr: 'TUE', index: '2' },
|
||||
{ value: WEDNESDAY, abbr: 'WED', index: '3' },
|
||||
{ value: THURSDAY, abbr: 'THU', index: '4' },
|
||||
{ value: FRIDAY, abbr: 'FRI', index: '5' },
|
||||
{ value: SATURDAY, abbr: 'SAT', index: '6' }
|
||||
];
|
||||
|
||||
export const WEEK_INDEX_REGEXP = new RegExp(WEEKS.map(({ index }) => `(?<!#)${index}`).join('|'), 'g');
|
||||
export const WEEK_LETTER_REGEXP = new RegExp(WEEKS.map(({ abbr }) => abbr).join('|'), 'g');
|
||||
|
||||
export const TYPE = {
|
||||
EVERY: '*',
|
||||
RANGE: '-',
|
||||
STEP: '/',
|
||||
SPECIFY: ',',
|
||||
UNSPECIFIC: '?',
|
||||
EMPTY: '',
|
||||
LAST_DAY: 'L',
|
||||
LAST_WEEKDAY: 'LW',
|
||||
WELL: '#',
|
||||
WEEKDAY: 'W'
|
||||
};
|
||||
|
||||
export const SECOND = 'second';
|
||||
export const MINUTE = 'minute';
|
||||
export const HOUR = 'hour';
|
||||
export const DATE = 'date';
|
||||
export const MONTH = 'month';
|
||||
export const WEEK = 'week';
|
||||
export const YEAR = 'year';
|
||||
|
||||
export const FIELDS: { value: I18n.FieldType; min: number; max: number }[] = [
|
||||
{ value: SECOND, min: MIN_SECOND, max: MAX_SECOND },
|
||||
{ value: MINUTE, min: MIN_MINUTE, max: MAX_MINUTE },
|
||||
{ value: HOUR, min: MIN_HOUR, max: MAX_HOUR },
|
||||
{ value: DATE, min: MIN_DATE, max: MAX_DATE },
|
||||
{ value: MONTH, min: MIN_MONTH, max: MAX_MONTH },
|
||||
{ value: WEEK, min: MIN_WEEK, max: MAX_WEEK },
|
||||
{ value: YEAR, min: MIN_YEAR, max: MAX_YEAR }
|
||||
];
|
||||
|
||||
export const LOCALE_EN: I18n.LocaleType = 'en-US';
|
||||
export const LOCALE_CN: I18n.LocaleType = 'zh-CN';
|
||||
export const DEFAULT_CRON_EXPRESSION = '* * * * * ?';
|
||||
export const DEFAULT_LOCALE: I18n.LocaleType = LOCALE_CN;
|
80
packages/cron-input/src/shared/utils.ts
Normal file
80
packages/cron-input/src/shared/utils.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
import I18n from '../i18n';
|
||||
import { WEEK, WEEKS, WEEK_INDEX_REGEXP, WEEK_LETTER_REGEXP } from './constants';
|
||||
|
||||
/**
|
||||
* 生成指定数组.
|
||||
*
|
||||
* @param min - 区间最小值.
|
||||
* @param max - 区间最大值.
|
||||
* @param labels - 显示名称.
|
||||
*/
|
||||
export function generateSpecifies(
|
||||
min: number,
|
||||
max: number,
|
||||
labels: string[] | null
|
||||
): { value: number; label: string }[] {
|
||||
const specifies = [];
|
||||
|
||||
let index = 0;
|
||||
|
||||
for (let specify = min; specify <= max; specify += 1) {
|
||||
specifies.push({ value: specify, label: labels ? labels[index] : specify.toString() });
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return specifies;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期时间字段补零.
|
||||
*
|
||||
* @param value - 待补零处理的数字.
|
||||
*/
|
||||
export function zerofill(value: number): string {
|
||||
const prefix = value < 10 ? '0' : '';
|
||||
|
||||
return `${prefix}${value}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换星期数字为缩写.
|
||||
*
|
||||
* @param value - .
|
||||
*/
|
||||
export function weekNumberToLetter(value: string): string {
|
||||
return value.replace(WEEK_INDEX_REGEXP, value => WEEKS.find(({ index }) => [index].includes(value))?.abbr || value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换星期缩写为数字.
|
||||
*
|
||||
* @param value - .
|
||||
*/
|
||||
export function weekLetterToNumber(value: string): string {
|
||||
return value.replace(WEEK_LETTER_REGEXP, value => WEEKS.find(({ abbr }) => abbr === value)?.index || value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 星期字段的 <a-input-number> 组件 formatter 函数.
|
||||
*
|
||||
* @param value - .
|
||||
* @param locale - 国际化字段.
|
||||
*/
|
||||
export function formatterWeek(value: string, locale: I18n.LocaleType): string {
|
||||
const day = WEEKS.find(({ index }) => index === value)?.value;
|
||||
|
||||
return I18n[locale].week[day!];
|
||||
}
|
||||
|
||||
/**
|
||||
* 星期字段的 <a-input-number> 组件 parser 函数.
|
||||
*
|
||||
* @param value - .
|
||||
* @param locale - 国际化字段.
|
||||
*/
|
||||
export function parserWeek(value: string, locale: I18n.LocaleType): number {
|
||||
const [key] = Object.entries(I18n[locale][WEEK]).find(([, val]) => val === value) as [string, string];
|
||||
|
||||
return WEEKS.findIndex(({ value }) => value === key);
|
||||
}
|
20
packages/cron-input/tsconfig.json
Normal file
20
packages/cron-input/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "naive-ui/volar"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
@ -20,6 +20,9 @@ importers:
|
||||
'@sa/color-palette':
|
||||
specifier: workspace:*
|
||||
version: link:packages/color-palette
|
||||
'@sa/cron-input':
|
||||
specifier: workspace:*
|
||||
version: link:packages/cron-input
|
||||
'@sa/hooks':
|
||||
specifier: workspace:*
|
||||
version: link:packages/hooks
|
||||
@ -188,6 +191,12 @@ importers:
|
||||
specifier: 2.9.3
|
||||
version: 2.9.3
|
||||
|
||||
packages/cron-input:
|
||||
dependencies:
|
||||
cron-parser:
|
||||
specifier: ^4.9.0
|
||||
version: 4.9.0
|
||||
|
||||
packages/hooks:
|
||||
dependencies:
|
||||
'@sa/axios':
|
||||
@ -3239,6 +3248,13 @@ packages:
|
||||
vary: 1.1.2
|
||||
dev: true
|
||||
|
||||
/cron-parser@4.9.0:
|
||||
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
dependencies:
|
||||
luxon: 3.4.4
|
||||
dev: false
|
||||
|
||||
/cross-spawn@7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -5644,6 +5660,11 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/luxon@3.4.4:
|
||||
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/magic-string@0.30.6:
|
||||
resolution: {integrity: sha512-n62qCLbPjNjyo+owKtveQxZFZTBm+Ms6YoGD23Wew6Vw337PElFNifQpknPruVRQV57kVShPnLGo9vWxVhpPvA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -45,10 +45,7 @@ export const generatedRoutes: GeneratedRoute[] = [
|
||||
component: 'layout.base$view.about',
|
||||
meta: {
|
||||
title: 'about',
|
||||
i18nKey: 'route.about',
|
||||
icon: 'fluent:book-information-24-regular',
|
||||
order: 10,
|
||||
hideInMenu: true
|
||||
i18nKey: 'route.about'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
1
src/typings/components.d.ts
vendored
1
src/typings/components.d.ts
vendored
@ -41,6 +41,7 @@ declare module 'vue' {
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||
|
Loading…
Reference in New Issue
Block a user