mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-27 06:41:12 +00:00
chore: 整理项目配置与代码优化
1. 统一前端环境变量命名,新增通用项目标题配置 2. 修复后端接口参数绑定方式,将Depends改为Query适配FastAPI解析 3. 优化表格列排序、搜索过滤功能与页面布局样式 4. 重构路由与状态管理代码,移除循环依赖 5. 更新依赖版本与脚本命令,修复构建警告 6. 调整多语言文案与快捷入口配置 7. 修复通知中心样式与接口返回格式
This commit is contained in:
@@ -29,36 +29,36 @@
|
||||
<!-- 卡片网格 -->
|
||||
<div v-else-if="!isEmpty" :style="{ padding: `0 ${gutter / 2}px` }">
|
||||
<ElRow :gutter="gutter">
|
||||
<ElCol
|
||||
v-for="(item, index) in items"
|
||||
:key="item[keyField] ?? index"
|
||||
:xs="xs"
|
||||
:sm="sm"
|
||||
:md="md"
|
||||
:lg="lg"
|
||||
:xl="xl"
|
||||
class="mb-4"
|
||||
>
|
||||
<ElCard
|
||||
class="fa-card"
|
||||
:class="cardClass"
|
||||
shadow="hover"
|
||||
:header-class="headerClass"
|
||||
:body-class="bodyClass"
|
||||
:footer-class="footerClass"
|
||||
@click="(e: MouseEvent) => emit('itemClick', item, e)"
|
||||
>
|
||||
<template v-if="$slots.header" #header>
|
||||
<slot name="header" :item="item" :index="index" />
|
||||
</template>
|
||||
<ElCol
|
||||
v-for="(item, index) in items"
|
||||
:key="item[keyField] ?? index"
|
||||
:xs="xs"
|
||||
:sm="sm"
|
||||
:md="md"
|
||||
:lg="lg"
|
||||
:xl="xl"
|
||||
class="mb-4"
|
||||
>
|
||||
<ElCard
|
||||
class="fa-card"
|
||||
:class="cardClass"
|
||||
shadow="hover"
|
||||
:header-class="headerClass"
|
||||
:body-class="bodyClass"
|
||||
:footer-class="footerClass"
|
||||
@click="(e: MouseEvent) => emit('itemClick', item, e)"
|
||||
>
|
||||
<template v-if="$slots.header" #header>
|
||||
<slot name="header" :item="item" :index="index" />
|
||||
</template>
|
||||
|
||||
<slot :item="item" :index="index" />
|
||||
<slot :item="item" :index="index" />
|
||||
|
||||
<template v-if="$slots.footer" #footer>
|
||||
<slot name="footer" :item="item" :index="index" />
|
||||
</template>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
<template v-if="$slots.footer" #footer>
|
||||
<slot name="footer" :item="item" :index="index" />
|
||||
</template>
|
||||
</ElCard>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,15 @@
|
||||
|
||||
<script setup lang="ts" generic="T extends Record<string, any>">
|
||||
import { computed } from "vue";
|
||||
import { ElCard, ElRow, ElCol, ElEmpty, ElSkeleton, ElSkeletonItem, ElScrollbar } from "element-plus";
|
||||
import {
|
||||
ElCard,
|
||||
ElRow,
|
||||
ElCol,
|
||||
ElEmpty,
|
||||
ElSkeleton,
|
||||
ElSkeletonItem,
|
||||
ElScrollbar,
|
||||
} from "element-plus";
|
||||
import FaPagination from "@/components/others/fa-pagination/index.vue";
|
||||
|
||||
defineOptions({ name: "FaCardGrid" });
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
:model-value="modelValue?.created_id == null ? undefined : modelValue.created_id"
|
||||
@update:model-value="
|
||||
(v: number | undefined) => {
|
||||
modelValue.value['created_id'] = v;
|
||||
modelValue['created_id'] = v;
|
||||
}
|
||||
"
|
||||
@confirm-click="emitImmediateSearch"
|
||||
@@ -50,7 +50,7 @@
|
||||
:model-value="modelValue?.updated_id == null ? undefined : modelValue.updated_id"
|
||||
@update:model-value="
|
||||
(v: number | undefined) => {
|
||||
modelValue.value['updated_id'] = v;
|
||||
modelValue['updated_id'] = v;
|
||||
}
|
||||
"
|
||||
@confirm-click="emitImmediateSearch"
|
||||
|
||||
@@ -124,6 +124,7 @@ import { resolveIconForFaSvgIcon } from "@utils";
|
||||
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { MagicStick, Position, Tools } from "@element-plus/icons-vue";
|
||||
import { useSettingsStore } from "@stores";
|
||||
import { AiChatAPI, ChatSession, ChatSessionDetail } from "@/api/module_ai/chat";
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
<ElRadio value="specify">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">指定</span>
|
||||
<ElCheckboxGroup v-model="fields.second.specified" :disabled="fields.second.mode !== 'specify'">
|
||||
<ElCheckboxGroup
|
||||
v-model="fields.second.specified"
|
||||
:disabled="fields.second.mode !== 'specify'"
|
||||
>
|
||||
<ElCheckbox v-for="v in secondOptions" :key="v" :label="v" :value="v" />
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
@@ -57,7 +60,10 @@
|
||||
<ElRadio value="specify">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">指定</span>
|
||||
<ElCheckboxGroup v-model="fields.minute.specified" :disabled="fields.minute.mode !== 'specify'">
|
||||
<ElCheckboxGroup
|
||||
v-model="fields.minute.specified"
|
||||
:disabled="fields.minute.mode !== 'specify'"
|
||||
>
|
||||
<ElCheckbox v-for="v in minuteOptions" :key="v" :label="v" :value="v" />
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
@@ -89,7 +95,10 @@
|
||||
<ElRadio value="specify">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">指定</span>
|
||||
<ElCheckboxGroup v-model="fields.hour.specified" :disabled="fields.hour.mode !== 'specify'">
|
||||
<ElCheckboxGroup
|
||||
v-model="fields.hour.specified"
|
||||
:disabled="fields.hour.mode !== 'specify'"
|
||||
>
|
||||
<ElCheckbox v-for="v in hourOptions" :key="v" :label="v" :value="v" />
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
@@ -130,7 +139,10 @@
|
||||
<ElRadio value="specify">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">指定</span>
|
||||
<ElCheckboxGroup v-model="fields.day.specified" :disabled="fields.day.mode !== 'specify'">
|
||||
<ElCheckboxGroup
|
||||
v-model="fields.day.specified"
|
||||
:disabled="fields.day.mode !== 'specify'"
|
||||
>
|
||||
<ElCheckbox v-for="v in dayOptions" :key="v" :label="v" :value="v" />
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
@@ -163,7 +175,10 @@
|
||||
<ElRadio value="specify">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">指定</span>
|
||||
<ElCheckboxGroup v-model="fields.month.specified" :disabled="fields.month.mode !== 'specify'">
|
||||
<ElCheckboxGroup
|
||||
v-model="fields.month.specified"
|
||||
:disabled="fields.month.mode !== 'specify'"
|
||||
>
|
||||
<ElCheckbox v-for="v in monthOptions" :key="v" :label="v" :value="v" />
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
@@ -200,7 +215,10 @@
|
||||
<ElRadio value="specify">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">指定</span>
|
||||
<ElCheckboxGroup v-model="fields.week.specified" :disabled="fields.week.mode !== 'specify'">
|
||||
<ElCheckboxGroup
|
||||
v-model="fields.week.specified"
|
||||
:disabled="fields.week.mode !== 'specify'"
|
||||
>
|
||||
<ElCheckbox v-for="v in weekOptions" :key="v" :label="v" :value="v" />
|
||||
</ElCheckboxGroup>
|
||||
</div>
|
||||
@@ -220,266 +238,293 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-(--el-text-color-secondary)">下次执行:</span>
|
||||
<span class="text-sm">{{ nextRun || '—' }}</span>
|
||||
<span class="text-sm">{{ nextRun || "—" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import cronParser from 'cron-parser'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import cronParser from "cron-parser";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface FieldState {
|
||||
mode: string
|
||||
rangeStart: string
|
||||
rangeEnd: string
|
||||
stepFrom: string
|
||||
stepInterval: string
|
||||
specified: string[]
|
||||
extra: string
|
||||
mode: string;
|
||||
rangeStart: string;
|
||||
rangeEnd: string;
|
||||
stepFrom: string;
|
||||
stepInterval: string;
|
||||
specified: string[];
|
||||
extra: string;
|
||||
}
|
||||
|
||||
interface Fields {
|
||||
second: FieldState
|
||||
minute: FieldState
|
||||
hour: FieldState
|
||||
day: FieldState
|
||||
month: FieldState
|
||||
week: FieldState
|
||||
second: FieldState;
|
||||
minute: FieldState;
|
||||
hour: FieldState;
|
||||
day: FieldState;
|
||||
month: FieldState;
|
||||
week: FieldState;
|
||||
}
|
||||
|
||||
const defaultField = (init: Partial<FieldState> = {}): FieldState => ({
|
||||
mode: 'every',
|
||||
rangeStart: '0',
|
||||
rangeEnd: '0',
|
||||
stepFrom: '0',
|
||||
stepInterval: '1',
|
||||
mode: "every",
|
||||
rangeStart: "0",
|
||||
rangeEnd: "0",
|
||||
stepFrom: "0",
|
||||
stepInterval: "1",
|
||||
specified: [],
|
||||
extra: '',
|
||||
extra: "",
|
||||
...init,
|
||||
})
|
||||
});
|
||||
|
||||
const fields = ref<Fields>({
|
||||
second: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
|
||||
minute: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
|
||||
hour: defaultField({ rangeStart: '0', rangeEnd: '23', stepFrom: '0' }),
|
||||
day: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '31', stepFrom: '1' }),
|
||||
month: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '12', stepFrom: '1' }),
|
||||
week: defaultField({ mode: 'any', rangeStart: '1', rangeEnd: '7', stepFrom: '1' }),
|
||||
})
|
||||
second: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
|
||||
minute: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
|
||||
hour: defaultField({ rangeStart: "0", rangeEnd: "23", stepFrom: "0" }),
|
||||
day: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "31", stepFrom: "1" }),
|
||||
month: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "12", stepFrom: "1" }),
|
||||
week: defaultField({ mode: "any", rangeStart: "1", rangeEnd: "7", stepFrom: "1" }),
|
||||
});
|
||||
|
||||
const activeTab = ref('秒')
|
||||
const activeTab = ref("秒");
|
||||
|
||||
const props = defineProps<{ modelValue?: string }>()
|
||||
const props = defineProps<{ modelValue?: string }>();
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
"update:modelValue": [value: string];
|
||||
}>();
|
||||
|
||||
const pad = (n: number) => n.toString().padStart(2, '0')
|
||||
const rangeOptions = (count: number, start = 0) => Array.from({ length: count }, (_, i) => pad(start + i))
|
||||
const secondOptions = rangeOptions(60)
|
||||
const minuteOptions = rangeOptions(60)
|
||||
const hourOptions = rangeOptions(24)
|
||||
const dayOptions = rangeOptions(31, 1)
|
||||
const monthOptions = rangeOptions(12, 1)
|
||||
const weekOptions = rangeOptions(7, 1)
|
||||
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||
const rangeOptions = (count: number, start = 0) =>
|
||||
Array.from({ length: count }, (_, i) => pad(start + i));
|
||||
const secondOptions = rangeOptions(60);
|
||||
const minuteOptions = rangeOptions(60);
|
||||
const hourOptions = rangeOptions(24);
|
||||
const dayOptions = rangeOptions(31, 1);
|
||||
const monthOptions = rangeOptions(12, 1);
|
||||
const weekOptions = rangeOptions(7, 1);
|
||||
|
||||
function toSegment(field: FieldState): string {
|
||||
switch (field.mode) {
|
||||
case 'every':
|
||||
return '*'
|
||||
case 'any':
|
||||
return '?'
|
||||
case 'range':
|
||||
return `${field.rangeStart}-${field.rangeEnd}`
|
||||
case 'step':
|
||||
return `${field.stepFrom}/${field.stepInterval}`
|
||||
case 'specify':
|
||||
return field.specified.map((v) => parseInt(v)).sort((a, b) => a - b).join(',') || '*'
|
||||
case 'last':
|
||||
return 'L'
|
||||
case 'lastWeekday':
|
||||
return `${field.extra}W`
|
||||
case 'lastWeek':
|
||||
return `${field.extra}L`
|
||||
case 'nthWeek':
|
||||
return `${field.rangeStart}#${field.rangeEnd}`
|
||||
case "every":
|
||||
return "*";
|
||||
case "any":
|
||||
return "?";
|
||||
case "range":
|
||||
return `${field.rangeStart}-${field.rangeEnd}`;
|
||||
case "step":
|
||||
return `${field.stepFrom}/${field.stepInterval}`;
|
||||
case "specify":
|
||||
return (
|
||||
field.specified
|
||||
.map((v) => parseInt(v))
|
||||
.sort((a, b) => a - b)
|
||||
.join(",") || "*"
|
||||
);
|
||||
case "last":
|
||||
return "L";
|
||||
case "lastWeekday":
|
||||
return `${field.extra}W`;
|
||||
case "lastWeek":
|
||||
return `${field.extra}L`;
|
||||
case "nthWeek":
|
||||
return `${field.rangeStart}#${field.rangeEnd}`;
|
||||
default:
|
||||
return '*'
|
||||
return "*";
|
||||
}
|
||||
}
|
||||
|
||||
const cron = computed(() => {
|
||||
const f = fields.value
|
||||
return `${toSegment(f.second)} ${toSegment(f.minute)} ${toSegment(f.hour)} ${toSegment(f.day)} ${toSegment(f.month)} ${toSegment(f.week)}`
|
||||
})
|
||||
const f = fields.value;
|
||||
return `${toSegment(f.second)} ${toSegment(f.minute)} ${toSegment(f.hour)} ${toSegment(f.day)} ${toSegment(f.month)} ${toSegment(f.week)}`;
|
||||
});
|
||||
|
||||
const cronDesc = computed(() => {
|
||||
const { second, minute, hour, day, month, week } = fields.value
|
||||
const W = ['', '周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
const { second, minute, hour, day, month, week } = fields.value;
|
||||
const W = ["", "周日", "周一", "周二", "周三", "周四", "周五", "周六"];
|
||||
|
||||
// 层级: 年 -> 月 -> 日/周 -> 时 -> 分 -> 秒
|
||||
const parts: string[] = []
|
||||
parts.push('每年')
|
||||
const parts: string[] = [];
|
||||
parts.push("每年");
|
||||
|
||||
// 月
|
||||
if (month.mode === 'specify' && month.specified.length) parts.push(`${month.specified.join('、')}月`)
|
||||
else parts.push('每月')
|
||||
if (month.mode === "specify" && month.specified.length)
|
||||
parts.push(`${month.specified.join("、")}月`);
|
||||
else parts.push("每月");
|
||||
|
||||
// 日/周
|
||||
if (day.mode === 'specify') parts.push(`${day.specified.join('、')}号`)
|
||||
else if (day.mode === 'last') parts.push('最后一天')
|
||||
else if (day.mode === 'lastWeekday') parts.push(`${day.extra}号最近工作日`)
|
||||
else if (week.mode === 'specify') parts.push(week.specified.map((v) => W[+v] || v).join('、'))
|
||||
else if (week.mode === 'range') parts.push(`${W[+week.rangeStart] || ''}至${W[+week.rangeEnd] || ''}`)
|
||||
else if (week.mode === 'lastWeek') parts.push(`最后一个${W[+week.extra] || ''}`)
|
||||
else if (week.mode === 'every') parts.push(day.mode === 'any' ? '每周' : '每天')
|
||||
else parts.push('每天')
|
||||
if (day.mode === "specify") parts.push(`${day.specified.join("、")}号`);
|
||||
else if (day.mode === "last") parts.push("最后一天");
|
||||
else if (day.mode === "lastWeekday") parts.push(`${day.extra}号最近工作日`);
|
||||
else if (week.mode === "specify") parts.push(week.specified.map((v) => W[+v] || v).join("、"));
|
||||
else if (week.mode === "range")
|
||||
parts.push(`${W[+week.rangeStart] || ""}至${W[+week.rangeEnd] || ""}`);
|
||||
else if (week.mode === "lastWeek") parts.push(`最后一个${W[+week.extra] || ""}`);
|
||||
else if (week.mode === "every") parts.push(day.mode === "any" ? "每周" : "每天");
|
||||
else parts.push("每天");
|
||||
|
||||
// 时
|
||||
if (hour.mode === 'every') parts.push('每时')
|
||||
else if (hour.mode === 'step') parts.push(`每${hour.stepInterval}时`)
|
||||
else if (hour.mode === 'specify' && hour.specified.length === 1) parts.push(`${hour.specified[0]}时`)
|
||||
if (hour.mode === "every") parts.push("每时");
|
||||
else if (hour.mode === "step") parts.push(`每${hour.stepInterval}时`);
|
||||
else if (hour.mode === "specify" && hour.specified.length === 1)
|
||||
parts.push(`${hour.specified[0]}时`);
|
||||
|
||||
// 分
|
||||
if (minute.mode === 'every') parts.push('每分')
|
||||
else if (minute.mode === 'step') parts.push(`每${minute.stepInterval}分`)
|
||||
else if (minute.mode === 'specify' && minute.specified.length === 1) parts.push(`${minute.specified[0]}分`)
|
||||
if (minute.mode === "every") parts.push("每分");
|
||||
else if (minute.mode === "step") parts.push(`每${minute.stepInterval}分`);
|
||||
else if (minute.mode === "specify" && minute.specified.length === 1)
|
||||
parts.push(`${minute.specified[0]}分`);
|
||||
|
||||
// 秒
|
||||
if (second.mode === 'every') parts.push('每秒')
|
||||
else if (second.mode === 'step') parts.push(`每${second.stepInterval}秒`)
|
||||
else if (second.mode === 'specify' && second.specified.length === 1) parts.push(`${second.specified[0]}秒`)
|
||||
if (second.mode === "every") parts.push("每秒");
|
||||
else if (second.mode === "step") parts.push(`每${second.stepInterval}秒`);
|
||||
else if (second.mode === "specify" && second.specified.length === 1)
|
||||
parts.push(`${second.specified[0]}秒`);
|
||||
|
||||
return parts.join(' ')
|
||||
})
|
||||
return parts.join(" ");
|
||||
});
|
||||
|
||||
let skipParse = false
|
||||
let skipParse = false;
|
||||
watch(cron, (val) => {
|
||||
skipParse = true
|
||||
emit('update:modelValue', val)
|
||||
nextTick(() => { skipParse = false })
|
||||
})
|
||||
skipParse = true;
|
||||
emit("update:modelValue", val);
|
||||
nextTick(() => {
|
||||
skipParse = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 解析 cron 表达式 → 回填 fields ----
|
||||
function parseSegment(seg: string, field: FieldState): void {
|
||||
if (seg === '*') {
|
||||
field.mode = 'every'
|
||||
} else if (seg === '?') {
|
||||
field.mode = 'any'
|
||||
} else if (seg === 'L') {
|
||||
field.mode = 'last'
|
||||
} else if (seg.includes('W')) {
|
||||
field.mode = 'lastWeekday'
|
||||
field.extra = seg.replace('W', '')
|
||||
} else if (seg.includes('L')) {
|
||||
field.mode = 'lastWeek'
|
||||
field.extra = seg.replace('L', '')
|
||||
} else if (seg.includes('#')) {
|
||||
field.mode = 'nthWeek'
|
||||
const [a, b] = seg.split('#')
|
||||
field.rangeStart = a!
|
||||
field.rangeEnd = b!
|
||||
} else if (seg.includes('-')) {
|
||||
field.mode = 'range'
|
||||
const [a, b] = seg.split('-')
|
||||
field.rangeStart = a!
|
||||
field.rangeEnd = b!
|
||||
} else if (seg.includes('/')) {
|
||||
field.mode = 'step'
|
||||
const [a, b] = seg.split('/')
|
||||
field.stepFrom = a!
|
||||
field.stepInterval = b!
|
||||
} else if (seg.includes(',')) {
|
||||
field.mode = 'specify'
|
||||
field.specified = seg.split(',').map((v) => pad(parseInt(v)))
|
||||
if (seg === "*") {
|
||||
field.mode = "every";
|
||||
} else if (seg === "?") {
|
||||
field.mode = "any";
|
||||
} else if (seg === "L") {
|
||||
field.mode = "last";
|
||||
} else if (seg.includes("W")) {
|
||||
field.mode = "lastWeekday";
|
||||
field.extra = seg.replace("W", "");
|
||||
} else if (seg.includes("L")) {
|
||||
field.mode = "lastWeek";
|
||||
field.extra = seg.replace("L", "");
|
||||
} else if (seg.includes("#")) {
|
||||
field.mode = "nthWeek";
|
||||
const [a, b] = seg.split("#");
|
||||
field.rangeStart = a!;
|
||||
field.rangeEnd = b!;
|
||||
} else if (seg.includes("-")) {
|
||||
field.mode = "range";
|
||||
const [a, b] = seg.split("-");
|
||||
field.rangeStart = a!;
|
||||
field.rangeEnd = b!;
|
||||
} else if (seg.includes("/")) {
|
||||
field.mode = "step";
|
||||
const [a, b] = seg.split("/");
|
||||
field.stepFrom = a!;
|
||||
field.stepInterval = b!;
|
||||
} else if (seg.includes(",")) {
|
||||
field.mode = "specify";
|
||||
field.specified = seg.split(",").map((v) => pad(parseInt(v)));
|
||||
} else if (/^\d+$/.test(seg)) {
|
||||
field.mode = 'specify'
|
||||
field.specified = [pad(parseInt(seg))]
|
||||
field.mode = "specify";
|
||||
field.specified = [pad(parseInt(seg))];
|
||||
}
|
||||
}
|
||||
|
||||
function applyCron(expr: string) {
|
||||
const parts = expr.trim().split(/\s+/)
|
||||
if (parts.length < 6) return
|
||||
const keys: (keyof Fields)[] = ['second', 'minute', 'hour', 'day', 'month', 'week']
|
||||
const parts = expr.trim().split(/\s+/);
|
||||
if (parts.length < 6) return;
|
||||
const keys: (keyof Fields)[] = ["second", "minute", "hour", "day", "month", "week"];
|
||||
keys.forEach((key, i) => {
|
||||
parseSegment(parts[i]!, fields.value[key])
|
||||
})
|
||||
parseSegment(parts[i]!, fields.value[key]);
|
||||
});
|
||||
// 自动定位到第一个非默认的 tab
|
||||
const idx = parts.findIndex((p) => p !== '*' && p !== '?')
|
||||
const tabMap = ['秒', '分钟', '小时', '日', '月', '周']
|
||||
activeTab.value = idx >= 0 ? tabMap[idx]! : '秒'
|
||||
const idx = parts.findIndex((p) => p !== "*" && p !== "?");
|
||||
const tabMap = ["秒", "分钟", "小时", "日", "月", "周"];
|
||||
activeTab.value = idx >= 0 ? tabMap[idx]! : "秒";
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (skipParse) return
|
||||
if (val) applyCron(val)
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (skipParse) return;
|
||||
if (val) applyCron(val);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// ---- 下一次执行时间 ----
|
||||
const nextRun = ref('')
|
||||
const nextRun = ref("");
|
||||
|
||||
function calcNextRun(expr: string) {
|
||||
if (!expr?.trim()) { nextRun.value = ''; return }
|
||||
if (!expr?.trim()) {
|
||||
nextRun.value = "";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parser = (cronParser as any).default ?? cronParser
|
||||
const parser = (cronParser as any).default ?? cronParser;
|
||||
const interval = parser.parse(expr, {
|
||||
currentDate: new Date(),
|
||||
})
|
||||
nextRun.value = dayjs(interval.next().toDate()).format('YYYY-MM-DD HH:mm:ss')
|
||||
});
|
||||
nextRun.value = dayjs(interval.next().toDate()).format("YYYY-MM-DD HH:mm:ss");
|
||||
} catch {
|
||||
nextRun.value = '无效表达式'
|
||||
nextRun.value = "无效表达式";
|
||||
}
|
||||
}
|
||||
|
||||
watch(cron, (val) => calcNextRun(val), { immediate: true })
|
||||
watch(cron, (val) => calcNextRun(val), { immediate: true });
|
||||
|
||||
const clear = () => {
|
||||
fields.value = {
|
||||
second: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
|
||||
minute: defaultField({ rangeStart: '0', rangeEnd: '59', stepFrom: '0' }),
|
||||
hour: defaultField({ rangeStart: '0', rangeEnd: '23', stepFrom: '0' }),
|
||||
day: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '31', stepFrom: '1' }),
|
||||
month: defaultField({ mode: 'every', rangeStart: '1', rangeEnd: '12', stepFrom: '1' }),
|
||||
week: defaultField({ mode: 'any', rangeStart: '1', rangeEnd: '7', stepFrom: '1' }),
|
||||
}
|
||||
activeTab.value = '秒'
|
||||
}
|
||||
second: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
|
||||
minute: defaultField({ rangeStart: "0", rangeEnd: "59", stepFrom: "0" }),
|
||||
hour: defaultField({ rangeStart: "0", rangeEnd: "23", stepFrom: "0" }),
|
||||
day: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "31", stepFrom: "1" }),
|
||||
month: defaultField({ mode: "every", rangeStart: "1", rangeEnd: "12", stepFrom: "1" }),
|
||||
week: defaultField({ mode: "any", rangeStart: "1", rangeEnd: "7", stepFrom: "1" }),
|
||||
};
|
||||
activeTab.value = "秒";
|
||||
};
|
||||
|
||||
defineExpose({ clear })
|
||||
defineExpose({ clear });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cron-selector {
|
||||
padding: 12px;
|
||||
|
||||
:deep(.el-tabs__content) { padding: 8px; }
|
||||
:deep(.el-tabs__content) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-radio-group) {
|
||||
.el-radio {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 6px;
|
||||
min-height: 28px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-checkbox-group) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.el-checkbox { margin-right: 6px; height: auto; margin-bottom: 6px; }
|
||||
|
||||
.el-checkbox {
|
||||
height: auto;
|
||||
margin-right: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cron-preview {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
margin-top: 12px;
|
||||
background-color: var(--el-fill-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user