mirror of
https://gitee.com/bootx/dax-pay-ui.git
synced 2025-09-03 10:56:00 +00:00
feat 新增 定时任务管理, 字典hooks,
This commit is contained in:
@@ -44,6 +44,7 @@
|
||||
"ant-design-vue": "^3.2.13",
|
||||
"axios": "^0.26.1",
|
||||
"codemirror": "^5.65.3",
|
||||
"cron-parser": "^4.6.0",
|
||||
"cropperjs": "^1.5.12",
|
||||
"crypto-js": "^4.1.1",
|
||||
"dayjs": "^1.11.1",
|
||||
|
@@ -1,4 +1,6 @@
|
||||
// 数字
|
||||
import { LabeledValueType } from 'ant-design-vue/lib/vc-tree-select/TreeSelect'
|
||||
|
||||
export const NUMBER = 'number'
|
||||
// 字符串
|
||||
export const STRING = 'string'
|
||||
@@ -23,12 +25,14 @@ export interface QueryField {
|
||||
placeholder?: string
|
||||
// 字段名称
|
||||
field: string
|
||||
// 栅格宽度
|
||||
md: number
|
||||
// 显示名称
|
||||
name: string
|
||||
// 精度
|
||||
precision?: number | null
|
||||
// 查询列表
|
||||
selectList?: Array<unknown> | null
|
||||
selectList?: LabeledValueType[] | null
|
||||
// 时间格式化
|
||||
format?: string | null
|
||||
}
|
||||
|
60
src/components/EasyCron/EasyCron.vue
Normal file
60
src/components/EasyCron/EasyCron.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}`">
|
||||
<EasyCronModal
|
||||
@register="registerModal"
|
||||
@ok="ok"
|
||||
v-model:value="editCronValue"
|
||||
:exeStartTime="exeStartTime"
|
||||
:hideYear="hideYear"
|
||||
:remote="remote"
|
||||
:hideSecond="hideSecond"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useDesign } from '/@/hooks/web/useDesign'
|
||||
import { useModal } from '/@/components/Modal'
|
||||
import { propTypes } from '/@/utils/propTypes'
|
||||
import EasyCronModal from './EasyCronModal.vue'
|
||||
import { cronEmits, cronProps } from './easy.cron.data'
|
||||
|
||||
const { prefixCls } = useDesign('easy-cron-input')
|
||||
const emit = defineEmits([...cronEmits, 'ok'])
|
||||
const props = defineProps({
|
||||
...cronProps,
|
||||
exeStartTime: propTypes.oneOfType([propTypes.number, propTypes.string, propTypes.object]).def(0),
|
||||
})
|
||||
const [registerModal, { openModal }] = useModal()
|
||||
const editCronValue = ref('* * * * * ? *')
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
editCronValue.value = '* * * * * ? *'
|
||||
return
|
||||
}
|
||||
if (newVal !== editCronValue.value) {
|
||||
editCronValue.value = newVal
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function showConfigModal() {
|
||||
if (!props.disabled) {
|
||||
openModal()
|
||||
}
|
||||
}
|
||||
function ok() {
|
||||
emit('ok', editCronValue.value)
|
||||
}
|
||||
defineExpose({
|
||||
showConfigModal,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import 'easy.cron.input';
|
||||
</style>
|
287
src/components/EasyCron/EasyCronInner.vue
Normal file
287
src/components/EasyCron/EasyCronInner.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}`">
|
||||
<div class="content">
|
||||
<a-tabs :size="`small`" v-model:activeKey="activeKey">
|
||||
<a-tab-pane tab="秒" key="second" v-if="!hideSecond">
|
||||
<SecondUI v-model:value="second" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="分" key="minute">
|
||||
<MinuteUI v-model:value="minute" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="时" key="hour">
|
||||
<HourUI v-model:value="hour" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="日" key="day">
|
||||
<DayUI v-model:value="day" :week="week" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="月" key="month">
|
||||
<MonthUI v-model:value="month" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="周" key="week">
|
||||
<WeekUI v-model:value="week" :day="day" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="年" key="year" v-if="!hideYear && !hideSecond">
|
||||
<YearUI v-model:value="year" :disabled="disabled" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<a-divider />
|
||||
<!-- 执行时间预览 -->
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="18" style="margin-top: 22px">
|
||||
<a-row :gutter="8">
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.second" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'second'">秒</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.minute" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'minute'">分</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.hour" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'hour'">时</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.day" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'day'">日</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.month" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'month'">月</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8" style="margin-bottom: 12px">
|
||||
<a-input v-model:value="inputValues.week" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'week'">周</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-input v-model:value="inputValues.year" @blur="onInputBlur">
|
||||
<template #addonBefore>
|
||||
<span class="allow-click" @click="activeKey = 'year'">年</span>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-input v-model:value="inputValues.cron" @blur="onInputCronBlur">
|
||||
<template #addonBefore>
|
||||
<a-tooltip title="Cron表达式">式</a-tooltip>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div>近十次执行时间(不含年)</div>
|
||||
<a-textarea type="textarea" :value="preTimeList" :rows="5" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch, provide } from 'vue'
|
||||
import { useDesign } from '/@/hooks/web/useDesign'
|
||||
import CronParser from 'cron-parser'
|
||||
import SecondUI from './tabs/SecondUI.vue'
|
||||
import MinuteUI from './tabs/MinuteUI.vue'
|
||||
import HourUI from './tabs/HourUI.vue'
|
||||
import DayUI from './tabs/DayUI.vue'
|
||||
import MonthUI from './tabs/MonthUI.vue'
|
||||
import WeekUI from './tabs/WeekUI.vue'
|
||||
import YearUI from './tabs/YearUI.vue'
|
||||
import { cronEmits, cronProps } from './easy.cron.data'
|
||||
import { debounce } from 'lodash-es'
|
||||
import XEUtils from 'xe-utils'
|
||||
|
||||
const { prefixCls } = useDesign('easy-cron-inner')
|
||||
provide('prefixCls', prefixCls)
|
||||
const emit = defineEmits([...cronEmits])
|
||||
const props = defineProps({ ...cronProps })
|
||||
const activeKey = ref(props.hideSecond ? 'minute' : 'second')
|
||||
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(() => {
|
||||
let result: string[] = []
|
||||
if (!props.hideSecond) {
|
||||
result.push(second.value ? second.value : '*')
|
||||
}
|
||||
result.push(minute.value ? minute.value : '*')
|
||||
result.push(hour.value ? hour.value : '*')
|
||||
result.push(day.value ? day.value : '*')
|
||||
result.push(month.value ? month.value : '*')
|
||||
result.push(week.value ? week.value : '?')
|
||||
if (!props.hideYear && !props.hideSecond) result.push(year.value ? year.value : '*')
|
||||
return result.join(' ')
|
||||
})
|
||||
// 不含年
|
||||
const cronValueNoYear = computed(() => {
|
||||
const v = cronValueInner.value
|
||||
if (props.hideYear || props.hideSecond) return v
|
||||
const vs = v.split(' ')
|
||||
if (vs.length >= 6) {
|
||||
// 转成 Quartz 的规则
|
||||
vs[5] = convertWeekToQuartz(vs[5])
|
||||
}
|
||||
return vs.slice(0, vs.length - 1).join(' ')
|
||||
})
|
||||
const calTriggerList = debounce(calTriggerListInner, 500)
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newVal) => {
|
||||
if (newVal === cronValueInner.value) {
|
||||
return
|
||||
}
|
||||
formatValue()
|
||||
},
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function formatValue() {
|
||||
if (!props.value) return
|
||||
const values = props.value.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) {
|
||||
let convert = (v: string) => {
|
||||
if (v === '0') {
|
||||
return '1'
|
||||
}
|
||||
if (v === '1') {
|
||||
return '0'
|
||||
}
|
||||
return (Number.parseInt(v) - 1).toString()
|
||||
}
|
||||
// 匹配示例 1-7 or 1/7
|
||||
let patten1 = /^([0-7])([-/])([0-7])$/
|
||||
// 匹配示例 1,4,7
|
||||
let 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) => {
|
||||
if (separator === '/') {
|
||||
return convert(before) + separator + after
|
||||
} else {
|
||||
return 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, +new Date(), (v) => {
|
||||
preTimeList.value = v
|
||||
})
|
||||
return
|
||||
}
|
||||
const format = 'yyyy-MM-dd hh:mm:ss'
|
||||
const options = {
|
||||
currentDate: XEUtils.toDateString(new Date(), format),
|
||||
}
|
||||
const iter = CronParser.parseExpression(cronValueNoYear.value, options)
|
||||
const result: string[] = []
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
result.push(XEUtils.toDateString(new Date(iter.next() as any), format))
|
||||
}
|
||||
preTimeList.value = result.length > 0 ? result.join('\n') : '无执行时间'
|
||||
}
|
||||
|
||||
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(event) {
|
||||
emitValue(event.target.value)
|
||||
}
|
||||
|
||||
function emitValue(value) {
|
||||
emit('change', value)
|
||||
emit('update:value', value)
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import 'easy.cron.inner';
|
||||
</style>
|
30
src/components/EasyCron/EasyCronModal.vue
Normal file
30
src/components/EasyCron/EasyCronModal.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<BasicModal @register="registerModal" title="Cron表达式" width="800px" @ok="onOk">
|
||||
<easy-cron-inner v-bind="attrs" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { useAttrs } from '/@/hooks/core/useAttrs'
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal'
|
||||
import EasyCronInner from './EasyCronInner.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EasyCronModal',
|
||||
components: { EasyCronInner, BasicModal },
|
||||
inheritAttrs: false,
|
||||
// emits: ['ok'],
|
||||
setup(props, ctx) {
|
||||
const attrs = useAttrs()
|
||||
const [registerModal, { closeModal }] = useModalInner()
|
||||
|
||||
function onOk() {
|
||||
closeModal()
|
||||
ctx.emit('ok')
|
||||
}
|
||||
|
||||
return { attrs, registerModal, onOk }
|
||||
},
|
||||
})
|
||||
</script>
|
21
src/components/EasyCron/LICENSE
Normal file
21
src/components/EasyCron/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 知行合一
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
10
src/components/EasyCron/easy.cron.data.ts
Normal file
10
src/components/EasyCron/easy.cron.data.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { propTypes } from '/@/utils/propTypes'
|
||||
|
||||
export const cronEmits = ['change', 'update:value']
|
||||
export const cronProps = {
|
||||
value: propTypes.string.def(''),
|
||||
disabled: propTypes.bool.def(false),
|
||||
hideSecond: propTypes.bool.def(false),
|
||||
hideYear: propTypes.bool.def(false),
|
||||
remote: propTypes.func,
|
||||
}
|
60
src/components/EasyCron/easy.cron.inner.less
Normal file
60
src/components/EasyCron/easy.cron.inner.less
Normal file
@@ -0,0 +1,60 @@
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-easy-cron-inner';
|
||||
|
||||
.@{prefix-cls} {
|
||||
.content {
|
||||
.ant-checkbox-wrapper + .ant-checkbox-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-config-list {
|
||||
text-align: left;
|
||||
margin: 0 10px 10px 10px;
|
||||
|
||||
.item {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span{
|
||||
padding: 0 2px
|
||||
}
|
||||
|
||||
.choice {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.w60 {
|
||||
// 不知为啥样式不生效. 这边强制覆盖下
|
||||
width: 60px !important;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.w80 {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.list-check-item {
|
||||
padding: 1px 3px;
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.list-cn .list-check-item {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
.tip-info {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.allow-click {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
14
src/components/EasyCron/easy.cron.input.less
Normal file
14
src/components/EasyCron/easy.cron.input.less
Normal file
@@ -0,0 +1,14 @@
|
||||
//noinspection LessUnresolvedVariable
|
||||
@prefix-cls: ~'@{namespace}-easy-cron-input';
|
||||
|
||||
.@{prefix-cls} {
|
||||
a.open-btn {
|
||||
cursor: pointer;
|
||||
|
||||
.app-iconify {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
6
src/components/EasyCron/index.ts
Normal file
6
src/components/EasyCron/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// 原开源项目地址:https://gitee.com/toktok/easy-cron
|
||||
|
||||
export { default as EasyCron } from './EasyCron.vue'
|
||||
export { default as EasyCronInner } from './EasyCronInner.vue'
|
||||
export { default as EasyCronModal } from './EasyCronModal.vue'
|
||||
export { default as CronValidator } from './validator'
|
93
src/components/EasyCron/tabs/DayUI.vue
Normal file
93
src/components/EasyCron/tabs/DayUI.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
|
||||
<span class="tip-info">日和周只能设置其中之一</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每日</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 日 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 日 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 日开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 日 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.work" v-bind="beforeRadioAttrs">工作日</a-radio>
|
||||
<span> 本月 </span>
|
||||
<InputNumber v-model:value="valueWork" v-bind="typeWorkAttrs" />
|
||||
<span> 日,最近的工作日 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.last" v-bind="beforeRadioAttrs">最后一日</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, watch } from 'vue'
|
||||
import { InputNumber } from 'ant-design-vue'
|
||||
import { TypeEnum, useTabEmits, useTabProps, useTabSetup } from './useTabMixin'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DayUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
props: {
|
||||
week: { type: String, default: '?' },
|
||||
},
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
const disabledChoice = computed(() => {
|
||||
return (props.week && props.week !== '?') || props.disabled
|
||||
})
|
||||
const setup = useTabSetup(props, context, {
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
return { ...setup, typeWorkAttrs }
|
||||
},
|
||||
})
|
||||
</script>
|
59
src/components/EasyCron/tabs/HourUI.vue
Normal file
59
src/components/EasyCron/tabs/HourUI.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每时</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 时 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 时 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 时开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 时 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { InputNumber } from 'ant-design-vue'
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HourUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 23,
|
||||
valueRange: { start: 0, end: 23 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
59
src/components/EasyCron/tabs/MinuteUI.vue
Normal file
59
src/components/EasyCron/tabs/MinuteUI.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每分</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 分 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 分 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 分开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 分 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { InputNumber } from 'ant-design-vue'
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MinuteUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 59,
|
||||
valueRange: { start: 0, end: 59 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
59
src/components/EasyCron/tabs/MonthUI.vue
Normal file
59
src/components/EasyCron/tabs/MonthUI.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每月</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 月 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 月 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 月开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 月 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { InputNumber } from 'ant-design-vue'
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MonthUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 1,
|
||||
maxValue: 12,
|
||||
valueRange: { start: 1, end: 12 },
|
||||
valueLoop: { start: 1, interval: 1 },
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
59
src/components/EasyCron/tabs/SecondUI.vue
Normal file
59
src/components/EasyCron/tabs/SecondUI.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每秒</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 秒 至 </span>
|
||||
<InputNumber v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 秒 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 秒开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 秒 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="i in specifyRange" :key="i">
|
||||
<a-checkbox :value="i" v-bind="typeSpecifyAttrs">{{ i }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { InputNumber } from 'ant-design-vue'
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SecondUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
maxValue: 59,
|
||||
valueRange: { start: 0, end: 59 },
|
||||
valueLoop: { start: 0, interval: 1 },
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
125
src/components/EasyCron/tabs/WeekUI.vue
Normal file
125
src/components/EasyCron/tabs/WeekUI.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.unset" v-bind="beforeRadioAttrs">不设置</a-radio>
|
||||
<span class="tip-info">日和周只能设置其中之一</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<a-select v-model:value="valueRange.start" :options="weekOptions" v-bind="typeRangeSelectAttrs" />
|
||||
<span> 至 </span>
|
||||
<a-select v-model:value="valueRange.end" :options="weekOptions" v-bind="typeRangeSelectAttrs" />
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<a-select v-model:value="valueLoop.start" :options="weekOptions" v-bind="typeLoopSelectAttrs" />
|
||||
<span> 开始,间隔 </span>
|
||||
<InputNumber v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 天 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.specify" v-bind="beforeRadioAttrs">指定</a-radio>
|
||||
<div class="list list-cn">
|
||||
<a-checkbox-group v-model:value="valueList">
|
||||
<template v-for="opt in weekOptions" :key="opt.value">
|
||||
<a-checkbox :value="opt.value" v-bind="typeSpecifyAttrs">{{ opt.label }}</a-checkbox>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, watch, defineComponent } from 'vue'
|
||||
import { InputNumber } from 'ant-design-vue'
|
||||
import { useTabProps, useTabEmits, useTabSetup, TypeEnum } from './useTabMixin'
|
||||
|
||||
const WEEK_MAP_EN = {
|
||||
'1': 'SUN',
|
||||
'2': 'MON',
|
||||
'3': 'TUE',
|
||||
'4': 'WED',
|
||||
'5': 'THU',
|
||||
'6': 'FRI',
|
||||
'7': 'SAT',
|
||||
}
|
||||
|
||||
const WEEK_MAP_CN = {
|
||||
'1': '周日',
|
||||
'2': '周一',
|
||||
'3': '周二',
|
||||
'4': '周三',
|
||||
'5': '周四',
|
||||
'6': '周五',
|
||||
'7': '周六',
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WeekUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '?',
|
||||
props: {
|
||||
day: { type: String, default: '*' },
|
||||
},
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
const disabledChoice = computed(() => {
|
||||
return (props.day && props.day !== '?') || props.disabled
|
||||
})
|
||||
const setup = useTabSetup(props, context, {
|
||||
defaultType: TypeEnum.unset,
|
||||
defaultValue: '?',
|
||||
minValue: 1,
|
||||
maxValue: 7,
|
||||
// 0,7表示周日 1表示周一
|
||||
valueRange: { start: 1, end: 7 },
|
||||
valueLoop: { start: 2, interval: 1 },
|
||||
disabled: disabledChoice,
|
||||
})
|
||||
const weekOptions = computed(() => {
|
||||
let options: { label: string; value: number }[] = []
|
||||
for (let weekKey of Object.keys(WEEK_MAP_CN)) {
|
||||
let weekName: string = WEEK_MAP_CN[weekKey]
|
||||
options.push({
|
||||
value: Number.parseInt(weekKey),
|
||||
label: weekName,
|
||||
})
|
||||
}
|
||||
return options
|
||||
})
|
||||
|
||||
const typeRangeSelectAttrs = computed(() => ({
|
||||
class: ['w80'],
|
||||
disabled: setup.typeRangeAttrs.value.disabled,
|
||||
}))
|
||||
|
||||
const typeLoopSelectAttrs = computed(() => ({
|
||||
class: ['w80'],
|
||||
disabled: setup.typeLoopAttrs.value.disabled,
|
||||
}))
|
||||
|
||||
watch(
|
||||
() => props.day,
|
||||
() => {
|
||||
setup.updateValue(disabledChoice.value ? '?' : setup.computeValue.value)
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
...setup,
|
||||
weekOptions,
|
||||
typeLoopSelectAttrs,
|
||||
typeRangeSelectAttrs,
|
||||
WEEK_MAP_CN,
|
||||
WEEK_MAP_EN,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
49
src/components/EasyCron/tabs/YearUI.vue
Normal file
49
src/components/EasyCron/tabs/YearUI.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div :class="`${prefixCls}-config-list`">
|
||||
<a-radio-group v-model:value="type">
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.every" v-bind="beforeRadioAttrs">每年</a-radio>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.range" v-bind="beforeRadioAttrs">区间</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber class="w80" v-model:value="valueRange.start" v-bind="typeRangeAttrs" />
|
||||
<span> 年 至 </span>
|
||||
<InputNumber class="w80" v-model:value="valueRange.end" v-bind="typeRangeAttrs" />
|
||||
<span> 年 </span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<a-radio :value="TypeEnum.loop" v-bind="beforeRadioAttrs">循环</a-radio>
|
||||
<span> 从 </span>
|
||||
<InputNumber class="w80" v-model:value="valueLoop.start" v-bind="typeLoopAttrs" />
|
||||
<span> 年开始,间隔 </span>
|
||||
<InputNumber class="w80" v-model:value="valueLoop.interval" v-bind="typeLoopAttrs" />
|
||||
<span> 年 </span>
|
||||
</div>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { InputNumber } from 'ant-design-vue'
|
||||
import { useTabProps, useTabEmits, useTabSetup } from './useTabMixin'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'YearUI',
|
||||
components: { InputNumber },
|
||||
props: useTabProps({
|
||||
defaultValue: '*',
|
||||
}),
|
||||
emits: useTabEmits(),
|
||||
setup(props, context) {
|
||||
const nowYear = new Date().getFullYear()
|
||||
return useTabSetup(props, context, {
|
||||
defaultValue: '*',
|
||||
minValue: 0,
|
||||
valueRange: { start: nowYear, end: nowYear + 100 },
|
||||
valueLoop: { start: nowYear, interval: 1 },
|
||||
})
|
||||
},
|
||||
})
|
||||
</script>
|
199
src/components/EasyCron/tabs/useTabMixin.ts
Normal file
199
src/components/EasyCron/tabs/useTabMixin.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
// 主要用于日和星期的互斥使用
|
||||
import { computed, inject, reactive, ref, unref, watch } from 'vue'
|
||||
import { propTypes } from '/@/utils/propTypes'
|
||||
|
||||
export enum TypeEnum {
|
||||
unset = 'UNSET',
|
||||
every = 'EVERY',
|
||||
range = 'RANGE',
|
||||
loop = 'LOOP',
|
||||
work = 'WORK',
|
||||
last = 'LAST',
|
||||
specify = 'SPECIFY',
|
||||
}
|
||||
|
||||
// use 公共 props
|
||||
export function useTabProps(options) {
|
||||
const defaultValue = options?.defaultValue ?? '?'
|
||||
return {
|
||||
value: propTypes.string.def(defaultValue),
|
||||
disabled: propTypes.bool.def(false),
|
||||
...options?.props,
|
||||
}
|
||||
}
|
||||
|
||||
// use 公共 emits
|
||||
export function useTabEmits() {
|
||||
return ['change', 'update:value']
|
||||
}
|
||||
|
||||
// use 公共 setup
|
||||
export function useTabSetup(props, context, options) {
|
||||
const { emit } = context
|
||||
const prefixCls = inject('prefixCls')
|
||||
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.unset:
|
||||
valueArray.push('?')
|
||||
break
|
||||
case TypeEnum.every:
|
||||
valueArray.push('*')
|
||||
break
|
||||
case TypeEnum.range:
|
||||
valueArray.push(`${valueRange.start}-${valueRange.end}`)
|
||||
break
|
||||
case TypeEnum.loop:
|
||||
valueArray.push(`${valueLoop.start}/${valueLoop.interval}`)
|
||||
break
|
||||
case TypeEnum.work:
|
||||
valueArray.push(`${valueWork.value}W`)
|
||||
break
|
||||
case TypeEnum.last:
|
||||
valueArray.push('L')
|
||||
break
|
||||
case TypeEnum.specify:
|
||||
if (valueList.value.length === 0) {
|
||||
valueList.value.push(minValue.value)
|
||||
}
|
||||
valueArray.push(valueList.value.join(','))
|
||||
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.value,
|
||||
(val) => {
|
||||
if (val !== computeValue.value) {
|
||||
parseValue(val)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(computeValue, (v) => updateValue(v))
|
||||
|
||||
function updateValue(value) {
|
||||
emit('change', value)
|
||||
emit('update:value', value)
|
||||
}
|
||||
|
||||
/**
|
||||
* parseValue
|
||||
* @param value
|
||||
*/
|
||||
function parseValue(value) {
|
||||
if (value === computeValue.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (!value || value === defaultValue.value) {
|
||||
type.value = TypeEnum.every
|
||||
} else if (value.indexOf('?') >= 0) {
|
||||
type.value = TypeEnum.unset
|
||||
} else if (value.indexOf('-') >= 0) {
|
||||
type.value = TypeEnum.range
|
||||
const values = value.split('-')
|
||||
if (values.length >= 2) {
|
||||
valueRange.start = parseInt(values[0])
|
||||
valueRange.end = parseInt(values[1])
|
||||
}
|
||||
} else if (value.indexOf('/') >= 0) {
|
||||
type.value = TypeEnum.loop
|
||||
const values = value.split('/')
|
||||
if (values.length >= 2) {
|
||||
valueLoop.start = value[0] === '*' ? 0 : parseInt(values[0])
|
||||
valueLoop.interval = parseInt(values[1])
|
||||
}
|
||||
} else if (value.indexOf('W') >= 0) {
|
||||
type.value = TypeEnum.work
|
||||
const values = value.split('W')
|
||||
if (!values[0] && !isNaN(values[0])) {
|
||||
valueWork.value = parseInt(values[0])
|
||||
}
|
||||
} else if (value.indexOf('L') >= 0) {
|
||||
type.value = TypeEnum.last
|
||||
} else if (value.indexOf(',') >= 0 || !isNaN(value)) {
|
||||
type.value = TypeEnum.specify
|
||||
valueList.value = value.split(',').map((item) => parseInt(item))
|
||||
} else {
|
||||
type.value = TypeEnum.every
|
||||
}
|
||||
} catch (e) {
|
||||
type.value = TypeEnum.every
|
||||
}
|
||||
}
|
||||
|
||||
const beforeRadioAttrs = computed(() => ({
|
||||
class: ['choice'],
|
||||
disabled: props.disabled || unref(options.disabled),
|
||||
}))
|
||||
const inputNumberAttrs = computed(() => ({
|
||||
class: ['w60'],
|
||||
max: maxValue.value,
|
||||
min: minValue.value,
|
||||
precision: 0,
|
||||
}))
|
||||
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),
|
||||
class: ['list-check-item'],
|
||||
}))
|
||||
|
||||
return {
|
||||
type,
|
||||
TypeEnum,
|
||||
prefixCls,
|
||||
defaultValue,
|
||||
valueRange,
|
||||
valueLoop,
|
||||
valueWeek,
|
||||
valueList,
|
||||
valueWork,
|
||||
maxValue,
|
||||
minValue,
|
||||
computeValue,
|
||||
specifyRange,
|
||||
updateValue,
|
||||
parseValue,
|
||||
beforeRadioAttrs,
|
||||
inputNumberAttrs,
|
||||
typeRangeAttrs,
|
||||
typeLoopAttrs,
|
||||
typeSpecifyAttrs,
|
||||
}
|
||||
}
|
47
src/components/EasyCron/validator.ts
Normal file
47
src/components/EasyCron/validator.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import CronParser from 'cron-parser'
|
||||
import type { ValidatorRule } from 'ant-design-vue/lib/form/interface'
|
||||
const cronRule: ValidatorRule = {
|
||||
validator({}, value) {
|
||||
// 没填写就不校验
|
||||
if (!value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
const values: string[] = value.split(' ').filter((item) => !!item)
|
||||
if (values.length > 7) {
|
||||
return Promise.reject('Cron表达式最多7项!')
|
||||
}
|
||||
// 检查第7项
|
||||
let val: string = value
|
||||
if (values.length === 7) {
|
||||
const year = values[6]
|
||||
if (year !== '*' && year !== '?') {
|
||||
let yearValues: string[] = []
|
||||
if (year.indexOf('-') >= 0) {
|
||||
yearValues = year.split('-')
|
||||
} else if (year.indexOf('/')) {
|
||||
yearValues = year.split('/')
|
||||
} else {
|
||||
yearValues = [year]
|
||||
}
|
||||
// 判断是否都是数字
|
||||
const checkYear = yearValues.some((item) => isNaN(Number(item)))
|
||||
if (checkYear) {
|
||||
return Promise.reject('Cron表达式参数[年]错误:' + year)
|
||||
}
|
||||
}
|
||||
// 取其中的前六项
|
||||
val = values.slice(0, 6).join(' ')
|
||||
}
|
||||
// 6位 没有年
|
||||
// 5位没有秒、年
|
||||
try {
|
||||
const iter = CronParser.parseExpression(val)
|
||||
iter.next()
|
||||
return Promise.resolve()
|
||||
} catch (e) {
|
||||
return Promise.reject('Cron表达式错误:' + e)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default cronRule.validator
|
@@ -8,6 +8,8 @@ import {
|
||||
Popconfirm,
|
||||
Select,
|
||||
SelectOption,
|
||||
Tabs,
|
||||
Checkbox,
|
||||
Switch,
|
||||
Tree,
|
||||
TreeSelect,
|
||||
@@ -37,6 +39,8 @@ export function registerGlobComp(app: App) {
|
||||
app.use(Layout)
|
||||
app.use(InputNumber)
|
||||
app.use(Tag)
|
||||
app.use(Tabs)
|
||||
app.use(Checkbox)
|
||||
app.use(Space)
|
||||
app.use(Modal)
|
||||
app.use(Drawer)
|
||||
|
@@ -1,7 +1,68 @@
|
||||
import { useDictStoreWithOut } from '/@/store/modules/dict'
|
||||
|
||||
const useDictStore = useDictStoreWithOut()
|
||||
|
||||
/**
|
||||
* 字典项转换
|
||||
*/
|
||||
function dictConvert(dictCode: string, code) {}
|
||||
function dictConvert(dictCode: string, code) {
|
||||
const dictList = useDictStore.getDict
|
||||
const item = dictList.filter((dict) => {
|
||||
return dictCode === dict.dictCode && dict.code === String(code)
|
||||
})
|
||||
if (item && item.length > 0) {
|
||||
return item[0].name
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典项列表
|
||||
*/
|
||||
export function dictItems(dictCode: string) {
|
||||
const dictList = useDictStore.getDict
|
||||
return dictList
|
||||
.filter((dict) => dictCode === dict.dictCode)
|
||||
.map((item) => {
|
||||
return { ...item, code: Number(item.code) }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典项列表(code值为数字)
|
||||
*/
|
||||
export function dictItemsNumber(dictCode: string) {
|
||||
const dictList = useDictStore.getDict
|
||||
return dictList
|
||||
.filter((dict) => dictCode === dict.dictCode)
|
||||
.map((item) => {
|
||||
return { ...item, code: Number(item.code) }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典下拉框数据列表
|
||||
*/
|
||||
function dictDropDown(dictCode: string) {
|
||||
const list = useDictStore.getDict
|
||||
return list
|
||||
.filter((dict) => dictCode === dict.dictCode)
|
||||
.map((o) => {
|
||||
return { label: o.name, value: o.code }
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 获取字典下拉框数据列表
|
||||
*/
|
||||
function dictDropDownNumber(dictCode: string) {
|
||||
const list = useDictStore.getDict
|
||||
return list
|
||||
.filter((dict) => dictCode === dict.dictCode)
|
||||
.map((o) => {
|
||||
return { label: o.name, value: Number(o.code) }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典hooks
|
||||
@@ -9,5 +70,9 @@ function dictConvert(dictCode: string, code) {}
|
||||
export function useDict() {
|
||||
return {
|
||||
dictConvert,
|
||||
dictItems,
|
||||
dictItemsNumber,
|
||||
dictDropDown,
|
||||
dictDropDownNumber,
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { FormEditType } from '/@/enums/formTypeEnum'
|
||||
import { unref } from "vue";
|
||||
import { unref } from 'vue'
|
||||
|
||||
/**
|
||||
* 服务器校验
|
||||
|
@@ -8,6 +8,7 @@ import { useUserStoreWithOut } from '/@/store/modules/user'
|
||||
import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic'
|
||||
|
||||
import { RootRoute } from '/@/router/routes'
|
||||
import { useDictStoreWithOut } from '/@/store/modules/dict'
|
||||
|
||||
const LOGIN_PATH = PageEnum.BASE_LOGIN
|
||||
|
||||
@@ -22,18 +23,18 @@ const whitePathList: PageEnum[] = [LOGIN_PATH]
|
||||
export function createPermissionGuard(router: Router) {
|
||||
const userStore = useUserStoreWithOut()
|
||||
const permissionStore = usePermissionStoreWithOut()
|
||||
const useDictStore = useDictStoreWithOut()
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (
|
||||
from.path === ROOT_PATH &&
|
||||
to.path === PageEnum.BASE_HOME
|
||||
&&
|
||||
// TODO 没有用户首页配置这个字段
|
||||
userStore.getUserInfo.homePath &&
|
||||
userStore.getUserInfo.homePath !== PageEnum.BASE_HOME
|
||||
) {
|
||||
next(userStore.getUserInfo.homePath)
|
||||
return
|
||||
}
|
||||
// if (
|
||||
// from.path === ROOT_PATH &&
|
||||
// to.path === PageEnum.BASE_HOME &&
|
||||
// // TODO 没有用户首页配置这个字段
|
||||
// userStore.getUserInfo.homePath &&
|
||||
// userStore.getUserInfo.homePath !== PageEnum.BASE_HOME
|
||||
// ) {
|
||||
// next(userStore.getUserInfo.homePath)
|
||||
// return
|
||||
// }
|
||||
|
||||
const token = userStore.getToken
|
||||
|
||||
@@ -77,12 +78,8 @@ export function createPermissionGuard(router: Router) {
|
||||
}
|
||||
|
||||
// Jump to the 404 page after processing the login
|
||||
if (
|
||||
from.path === LOGIN_PATH &&
|
||||
to.name === PAGE_NOT_FOUND_ROUTE.name &&
|
||||
to.fullPath !== (userStore.getUserInfo.homePath || PageEnum.BASE_HOME)
|
||||
) {
|
||||
next(userStore.getUserInfo.homePath || PageEnum.BASE_HOME)
|
||||
if (from.path === LOGIN_PATH && to.name === PAGE_NOT_FOUND_ROUTE.name && to.fullPath !== PageEnum.BASE_HOME) {
|
||||
next(PageEnum.BASE_HOME)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,11 +97,15 @@ export function createPermissionGuard(router: Router) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
//TODO 添加 websocket连接. 字典信息缓存
|
||||
//TODO 添加 websocket连接.
|
||||
console.log(`路由守卫`)
|
||||
|
||||
// 重载菜单
|
||||
const routes = await permissionStore.buildRoutesAction()
|
||||
|
||||
// 初始化字典
|
||||
await useDictStore.initDict()
|
||||
|
||||
routes.forEach((route) => {
|
||||
router.addRoute(route as unknown as RouteRecordRaw)
|
||||
})
|
||||
|
36
src/store/modules/dict.ts
Normal file
36
src/store/modules/dict.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { findAll } from '/@/views/modules/system/dict/DictItem.api'
|
||||
import { Dict } from '/#/store'
|
||||
import { store } from '/@/store'
|
||||
|
||||
interface DictState {
|
||||
dict: Dict[]
|
||||
}
|
||||
export const useDictStore = defineStore({
|
||||
id: 'app-dict',
|
||||
state: (): DictState => ({
|
||||
dict: [],
|
||||
}),
|
||||
getters: {
|
||||
getDict(): Dict[] {
|
||||
return this.dict
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async initDict() {
|
||||
findAll().then(({ data }) => {
|
||||
this.dict = data.map((o) => {
|
||||
return {
|
||||
dictCode: o.dictCode,
|
||||
code: o.code,
|
||||
name: o.name,
|
||||
} as Dict
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
// Need to be used outside the setup
|
||||
export function useDictStoreWithOut() {
|
||||
return useDictStore(store)
|
||||
}
|
@@ -15,7 +15,11 @@
|
||||
<vxe-column type="seq" width="60" />
|
||||
<vxe-column field="code" title="编码" />
|
||||
<vxe-column field="name" title="名称" />
|
||||
<vxe-column field="groupTag" title="分类标签" />
|
||||
<vxe-column field="groupTag" title="分类标签">
|
||||
<template #default="{ row }">
|
||||
<a-tag color="green">{{ row.groupTag || '空' }}</a-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="remark" title="备注" />
|
||||
<vxe-column field="createTime" title="创建时间" />
|
||||
<vxe-column fixed="right" width="220" :showOverflow="false" title="操作">
|
||||
|
@@ -71,7 +71,7 @@
|
||||
<a href="javascript:" @click="resourcePage(row)">权限资源</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-dropdown>
|
||||
<a class="ant-dropdown-link"> 更多 </a>
|
||||
<a> 更多 <icon icon="ant-design:down-outlined" :size="12" /> </a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
width="60%"
|
||||
placement="right"
|
||||
:closable="true"
|
||||
@close="handleCancel"
|
||||
@close="visible = false"
|
||||
>
|
||||
<vxe-toolbar ref="xToolbar" custom zoom :refresh="{ query: init }">
|
||||
<template #buttons>
|
||||
@@ -94,10 +94,6 @@
|
||||
del(record.id).then(() => queryPage())
|
||||
}
|
||||
|
||||
// 关闭
|
||||
function handleCancel() {
|
||||
visible = false
|
||||
}
|
||||
defineExpose({
|
||||
init,
|
||||
})
|
||||
|
@@ -27,7 +27,7 @@
|
||||
:options="paramTypeList"
|
||||
style="width: 220px"
|
||||
:disabled="showable"
|
||||
v-model="form.type"
|
||||
v-model:value="form.type"
|
||||
placeholder="请选择状态"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -53,6 +53,7 @@
|
||||
import { FormEditType } from '/@/enums/formTypeEnum'
|
||||
import { BasicModal } from '/@/components/Modal'
|
||||
import { useValidate } from '/@/hooks/bootx/useValidate'
|
||||
import { useDict } from '/@/hooks/bootx/useDict'
|
||||
|
||||
const {
|
||||
initFormModel,
|
||||
@@ -69,6 +70,7 @@
|
||||
formEditType,
|
||||
} = useFormEdit()
|
||||
const { existsByServer } = useValidate()
|
||||
const { dictDropDownNumber } = useDict()
|
||||
|
||||
// 表单
|
||||
const formRef = $ref<FormInstance>()
|
||||
@@ -77,12 +79,12 @@
|
||||
name: '',
|
||||
paramKey: '',
|
||||
value: '',
|
||||
type: 2,
|
||||
type: 1,
|
||||
internal: false,
|
||||
remark: '',
|
||||
} as SystemParam)
|
||||
// 参数类型
|
||||
let paramTypeList = $ref([])
|
||||
let paramTypeList = dictDropDownNumber('ParamType')
|
||||
// 校验
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: '参数名称必填', trigger: ['blur', 'change'] }],
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<a-space>
|
||||
<a-button type="primary" @click="add">新建</a-button>
|
||||
<a-dropdown v-if="batchOperateFlag">
|
||||
<a-button>批量操作 <icon icon="ant-design:down-outlined" /></a-button>
|
||||
<a-button pre-icon="ant-design:down-outlined">批量操作</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
@@ -25,7 +25,7 @@
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-popconfirm title="是否同步系统请求资源" @confirm="sync()" okText="是" cancelText="否">
|
||||
<a-button><icon icon="ant-design:sync-outlined" />同步系统资源</a-button>
|
||||
<a-button pre-icon="ant-design:sync-outlined">同步系统资源</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
|
130
src/views/modules/system/quartz/QuartzJob.api.ts
Normal file
130
src/views/modules/system/quartz/QuartzJob.api.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { defHttp } from '/@/utils/http/axios'
|
||||
import { PageResult, Result } from '/#/axios'
|
||||
import { BaseEntity } from '/#/web'
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
export const page = (params) => {
|
||||
return defHttp.get<Result<PageResult<QuartzJob>>>({
|
||||
url: '/quartz/page',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条
|
||||
*/
|
||||
export const get = (id) => {
|
||||
return defHttp.get<Result<QuartzJob>>({
|
||||
url: '/quartz/findById',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
export const add = (obj: QuartzJob) => {
|
||||
return defHttp.post({
|
||||
url: '/quartz/add',
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
export const update = (obj: QuartzJob) => {
|
||||
return defHttp.post({
|
||||
url: '/quartz/update',
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
export const del = (id) => {
|
||||
return defHttp.delete({
|
||||
url: '/quartz/delete',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部
|
||||
*/
|
||||
export const findAll = () => {
|
||||
return defHttp.get<Result<Array<QuartzJob>>>({
|
||||
url: '/quartz/findAll',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始
|
||||
* @param id
|
||||
*/
|
||||
export function start(id) {
|
||||
return defHttp.post({
|
||||
url: '/quartz/start',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止
|
||||
*/
|
||||
export function stop(id) {
|
||||
return defHttp.post({
|
||||
url: '/quartz/stop',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即执行
|
||||
*/
|
||||
export function execute(id) {
|
||||
return defHttp.post({
|
||||
url: '/quartz/execute',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步任务状态
|
||||
*/
|
||||
export const syncJobStatus = () => {
|
||||
return defHttp.post({
|
||||
url: '/quartz/syncJobStatus',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是定时任务类
|
||||
*/
|
||||
export function judgeJobClass(jobClassName) {
|
||||
return defHttp.get<Result<string>>({
|
||||
url: '/quartz/judgeJobClass',
|
||||
params: { jobClassName },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时任务
|
||||
*/
|
||||
export interface QuartzJob extends BaseEntity {
|
||||
// 任务名称
|
||||
name: string
|
||||
// 任务类名
|
||||
jobClassName: string
|
||||
// cron表达式
|
||||
cron: string
|
||||
// 参数
|
||||
parameter: string
|
||||
// 状态
|
||||
state: number
|
||||
// 备注
|
||||
remark: string
|
||||
}
|
163
src/views/modules/system/quartz/QuartzJobEdit.vue
Normal file
163
src/views/modules/system/quartz/QuartzJobEdit.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<basic-modal
|
||||
v-bind="$attrs"
|
||||
:loading="confirmLoading"
|
||||
:width="modalWidth"
|
||||
:title="title"
|
||||
:visible="visible"
|
||||
:mask-closable="showable"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form class="small-from-item" ref="formRef" :model="form" :rules="rules" :label-col="labelCol" :wrapper-col="wrapperCol">
|
||||
<a-form-item label="主键" :hidden="true">
|
||||
<a-input v-model:value="form.id" :disabled="showable" />
|
||||
</a-form-item>
|
||||
<a-form-item label="任务名称" name="name">
|
||||
<a-input v-model:value="form.name" :disabled="showable" placeholder="请输入任务名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="任务类名" name="jobClassName">
|
||||
<a-input v-model:value="form.jobClassName" :disabled="showable" placeholder="请输入任务类名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="cron表达式" name="cron">
|
||||
<a-input placeholder="请选择cron表达式" v-model:value="form.cron" disabled>
|
||||
<template #addonAfter>
|
||||
<a class="open-btn" :disabled="showable ? 'disabled' : null" @click="showConfigModal">
|
||||
<icon icon="ant-design:setting-outlined" />
|
||||
<span>选择</span>
|
||||
</a>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="参数" name="parameter">
|
||||
<a-textarea v-model:value="form.parameter" :disabled="showable" placeholder="请输入参数" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="state" v-if="showable">
|
||||
<a-tag v-if="form.state === 1" color="green">运行</a-tag>
|
||||
<a-tag v-else color="red">停止</a-tag>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="form.remark" :disabled="showable" placeholder="请输入备注" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button key="cancel" @click="handleCancel">取消</a-button>
|
||||
<a-button v-if="!showable" key="forward" :loading="confirmLoading" type="primary" @click="handleOk">保存</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<easy-cron ref="easyCron" :value="form.cron" @ok="cronOk" />
|
||||
</basic-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, unref } from 'vue'
|
||||
import { $ref } from 'vue/macros'
|
||||
import useFormEdit from '/@/hooks/bootx/useFormEdit'
|
||||
import { add, get, update, QuartzJob, judgeJobClass } from './QuartzJob.api'
|
||||
import { FormInstance, Rule } from 'ant-design-vue/lib/form'
|
||||
import { FormEditType } from '/@/enums/formTypeEnum'
|
||||
import { BasicModal, useModal } from '/@/components/Modal'
|
||||
import Icon from '/@/components/Icon/src/Icon.vue'
|
||||
import EasyCron from '/@/components/EasyCron/EasyCron.vue'
|
||||
|
||||
const {
|
||||
initFormModel,
|
||||
handleCancel,
|
||||
search,
|
||||
labelCol,
|
||||
wrapperCol,
|
||||
modalWidth,
|
||||
title,
|
||||
confirmLoading,
|
||||
visible,
|
||||
editable,
|
||||
showable,
|
||||
formEditType,
|
||||
} = useFormEdit()
|
||||
const [registerModal, { openModal }] = useModal()
|
||||
|
||||
// 表单
|
||||
const formRef = $ref<FormInstance>()
|
||||
let form = $ref({
|
||||
id: null,
|
||||
name: '',
|
||||
jobClassName: '',
|
||||
cron: '',
|
||||
parameter: '',
|
||||
state: 1,
|
||||
remark: '',
|
||||
} as QuartzJob)
|
||||
// 校验
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: '请输入任务名称' }],
|
||||
jobClassName: [
|
||||
{ required: true, message: '请输入任务类名' },
|
||||
{ validator: validateJobClass, trigger: 'blur' },
|
||||
],
|
||||
cron: [{ required: true, message: '请输入或选择cron' }],
|
||||
} as Record<string, Rule[]>)
|
||||
const easyCron = $ref<any>()
|
||||
// 事件
|
||||
const emits = defineEmits(['ok'])
|
||||
// 入口
|
||||
function init(id, editType: FormEditType) {
|
||||
initFormModel(id, editType)
|
||||
resetForm()
|
||||
getInfo(id, editType)
|
||||
}
|
||||
// 获取信息
|
||||
function getInfo(id, editType: FormEditType) {
|
||||
if ([FormEditType.Edit, FormEditType.Show].includes(editType)) {
|
||||
confirmLoading.value = true
|
||||
get(id).then(({ data }) => {
|
||||
form = data
|
||||
confirmLoading.value = false
|
||||
})
|
||||
} else {
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
// 保存
|
||||
function handleOk() {
|
||||
formRef.validate().then(async () => {
|
||||
confirmLoading.value = true
|
||||
if (formEditType.value === FormEditType.Add) {
|
||||
await add(form)
|
||||
} else if (formEditType.value === FormEditType.Edit) {
|
||||
await update(form)
|
||||
}
|
||||
confirmLoading.value = false
|
||||
handleCancel()
|
||||
emits('ok')
|
||||
})
|
||||
}
|
||||
|
||||
// 重置表单的校验
|
||||
function resetForm() {
|
||||
nextTick(() => {
|
||||
formRef.resetFields()
|
||||
})
|
||||
}
|
||||
function showConfigModal() {
|
||||
easyCron.showConfigModal()
|
||||
}
|
||||
|
||||
function cronOk(cron) {
|
||||
form.cron = unref(cron)
|
||||
formRef.validateFields('cron')
|
||||
}
|
||||
|
||||
async function validateJobClass() {
|
||||
const { jobClassName } = form
|
||||
if (!jobClassName) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
const res = await judgeJobClass(jobClassName)
|
||||
return res.data ? Promise.reject(res.data) : Promise.resolve()
|
||||
}
|
||||
defineExpose({
|
||||
init,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
211
src/views/modules/system/quartz/QuartzJobList.vue
Normal file
211
src/views/modules/system/quartz/QuartzJobList.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="m-3 p-3 pt-5 bg-white">
|
||||
<b-query :query-params="model.queryParam" :fields="fields" @query="queryPage" @reset="resetQueryParams" />
|
||||
</div>
|
||||
<div class="m-3 p-3 bg-white">
|
||||
<vxe-toolbar ref="xToolbar" custom :refresh="{ query: queryPage }">
|
||||
<template #buttons>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="add">新建</a-button>
|
||||
<a-button pre-icon="ant-design:sync-outlined" @click="syncJobStatus">状态同步</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
<vxe-table row-id="id" ref="xTable" :data="pagination.records" :loading="loading">
|
||||
<vxe-column type="seq" width="60" />
|
||||
<vxe-column field="name" title="任务名称" />
|
||||
<vxe-column field="jobClassName" title="任务类名" />
|
||||
<vxe-column field="cron" title="cron表达式" />
|
||||
<vxe-column field="parameter" title="参数" />
|
||||
<vxe-column field="state" title="状态">
|
||||
<template #default="{ row }">
|
||||
<a-tag v-if="row.state === 1" color="green">运行</a-tag>
|
||||
<a-tag v-else color="red">停用</a-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="remark" title="备注" />
|
||||
<vxe-column field="createTime" title="创建时间" />
|
||||
<vxe-column fixed="right" width="210" :showOverflow="false" title="操作">
|
||||
<template #default="{ row }">
|
||||
<span>
|
||||
<a href="javascript:" @click="show(row)">查看</a>
|
||||
</span>
|
||||
<a-divider type="vertical" />
|
||||
<span>
|
||||
<a href="javascript:" @click="edit(row)">编辑</a>
|
||||
</span>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm title="是否删除" @confirm="remove(row)" okText="是" cancelText="否">
|
||||
<a href="javascript:" style="color: red">删除</a>
|
||||
</a-popconfirm>
|
||||
<a-divider type="vertical" />
|
||||
<a-dropdown>
|
||||
<a> 更多 <icon icon="ant-design:down-outlined" :size="12" /> </a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
<a href="javascript:" @click="logPage(row)">执行日志</a>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a href="javascript:" @click="executeJob(row)">立即运行</a>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="row.state === 0">
|
||||
<a href="javascript:" @click="startJob(row)">启动</a>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="row.state === 1">
|
||||
<a href="javascript:" @click="startJob(row)" style="color: red">停止</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
<vxe-pager
|
||||
size="medium"
|
||||
:loading="loading"
|
||||
:current-page="pagination.current"
|
||||
:page-size="pagination.size"
|
||||
:total="pagination.total"
|
||||
@page-change="handleTableChange"
|
||||
/>
|
||||
<quartz-job-log-list ref="quartzJobLogList" />
|
||||
<quartz-job-edit ref="quartzJobEdit" @ok="queryPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { $ref } from 'vue/macros'
|
||||
import { del, execute, page, start, stop, syncJobStatus } from './QuartzJob.api'
|
||||
import useTablePage from '/@/hooks/bootx/useTablePage'
|
||||
import QuartzJobEdit from './QuartzJobEdit.vue'
|
||||
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
|
||||
import BQuery from '/@/components/Bootx/Query/BQuery.vue'
|
||||
import { FormEditType } from '/@/enums/formTypeEnum'
|
||||
import { useMessage } from '/@/hooks/web/useMessage'
|
||||
import { QueryField } from '/@/components/Bootx/Query/Query'
|
||||
import Icon from '/@/components/Icon/src/Icon.vue'
|
||||
import QuartzJobLogList from './QuartzJobLogList.vue'
|
||||
|
||||
// 使用hooks
|
||||
const { handleTableChange, pageQueryResHandel, resetQueryParams, pagination, pages, model, loading } = useTablePage(queryPage)
|
||||
const { notification, createMessage, createConfirm } = useMessage()
|
||||
|
||||
// 查询条件
|
||||
const fields = [
|
||||
{ field: 'jobClassName', type: 'string', name: '任务类名', placeholder: '请输入任务类名' },
|
||||
{
|
||||
field: 'status',
|
||||
type: 'list',
|
||||
name: '任务状态',
|
||||
placeholder: '请选择状态',
|
||||
md: 5,
|
||||
selectList: [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '正常', value: '1' },
|
||||
{ label: '停止', value: '0' },
|
||||
],
|
||||
},
|
||||
] as QueryField[]
|
||||
|
||||
const xTable = $ref<VxeTableInstance>()
|
||||
const xToolbar = $ref<VxeToolbarInstance>()
|
||||
const quartzJobEdit = $ref<any>()
|
||||
const quartzJobLogList = $ref<any>()
|
||||
|
||||
onMounted(() => {
|
||||
vxeBind()
|
||||
queryPage()
|
||||
})
|
||||
function vxeBind() {
|
||||
xTable.connect(xToolbar)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
function queryPage() {
|
||||
loading.value = true
|
||||
page({
|
||||
...model.queryParam,
|
||||
...pages,
|
||||
}).then(({ data }) => {
|
||||
pageQueryResHandel(data)
|
||||
})
|
||||
}
|
||||
// 新增
|
||||
function add() {
|
||||
quartzJobEdit.init(null, FormEditType.Add)
|
||||
}
|
||||
// 查看
|
||||
function edit(record) {
|
||||
quartzJobEdit.init(record.id, FormEditType.Edit)
|
||||
}
|
||||
// 查看
|
||||
function show(record) {
|
||||
quartzJobEdit.init(record.id, FormEditType.Show)
|
||||
}
|
||||
// 查看执行日志
|
||||
function logPage(record) {
|
||||
quartzJobLogList.init(record)
|
||||
}
|
||||
|
||||
// 删除
|
||||
function remove(record) {
|
||||
del(record.id).then(() => {
|
||||
createMessage.success('删除成功')
|
||||
})
|
||||
queryPage()
|
||||
}
|
||||
// 开始
|
||||
function startJob(record) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '警告',
|
||||
content: '是否停止定时任务',
|
||||
onOk: () => {
|
||||
start(record.id).then(() => {
|
||||
createMessage.success('启动成功')
|
||||
queryPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
// 停止
|
||||
function stopJob(record) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '警告',
|
||||
content: '是否删除该条数据',
|
||||
onOk: () => {
|
||||
stop(record.id).then(() => {
|
||||
createMessage.success('停止成功')
|
||||
queryPage()
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
// 立即执行
|
||||
function executeJob(record) {
|
||||
createConfirm({
|
||||
iconType: 'warning',
|
||||
title: '警告',
|
||||
content: '是否立即运行该定时任务',
|
||||
onOk: () => {
|
||||
execute(record.id).then(() => {
|
||||
createMessage.success('运行成功')
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 同步任务状态
|
||||
function sync() {
|
||||
syncJobStatus().then(() => {
|
||||
createMessage.success('任务状态同步成功')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
82
src/views/modules/system/quartz/QuartzJobLog.api.ts
Normal file
82
src/views/modules/system/quartz/QuartzJobLog.api.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { defHttp } from '/@/utils/http/axios'
|
||||
import { PageResult, Result } from '/#/axios'
|
||||
import { BaseEntity } from '/#/web'
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*/
|
||||
export const page = (params) => {
|
||||
return defHttp.get<Result<PageResult<QuartzJobLog>>>({
|
||||
url: '/quartz/log/page',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单条
|
||||
*/
|
||||
export const get = (id) => {
|
||||
return defHttp.get<Result<QuartzJobLog>>({
|
||||
url: '/quartz/log/findById',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*/
|
||||
export const add = (obj: QuartzJobLog) => {
|
||||
return defHttp.post({
|
||||
url: '/quartz/log/add',
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*/
|
||||
export const update = (obj: QuartzJobLog) => {
|
||||
return defHttp.post({
|
||||
url: '/quartz/log/update',
|
||||
data: obj,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
export const del = (id) => {
|
||||
return defHttp.delete({
|
||||
url: '/quartz/log/delete',
|
||||
params: { id },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部
|
||||
*/
|
||||
export const findAll = () => {
|
||||
return defHttp.get<Result<Array<QuartzJobLog>>>({
|
||||
url: '/quartz/log/findAll',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务执行日志
|
||||
*/
|
||||
export interface QuartzJobLog extends BaseEntity {
|
||||
// 处理器名称
|
||||
handlerName: string
|
||||
// 处理器全限定名
|
||||
className: string
|
||||
// 是否执行成功
|
||||
success: boolean
|
||||
// 错误信息
|
||||
errorMessage: string
|
||||
// 开始时间
|
||||
startTime: string
|
||||
// 结束时间
|
||||
endTime: string
|
||||
// 执行时长
|
||||
duration: number
|
||||
}
|
110
src/views/modules/system/quartz/QuartzJobLogList.vue
Normal file
110
src/views/modules/system/quartz/QuartzJobLogList.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
forceRender
|
||||
title="任务执行日志"
|
||||
:visible="visible"
|
||||
:maskClosable="true"
|
||||
width="60%"
|
||||
placement="right"
|
||||
:closable="true"
|
||||
@close="visible = false"
|
||||
>
|
||||
<b-query :query-params="model.queryParam" :fields="fields" @query="queryPage" @reset="resetQueryParams" />
|
||||
<vxe-toolbar ref="xToolbar" custom :refresh="{ query: queryPage }" />
|
||||
<vxe-table row-id="id" ref="xTable" :data="pagination.records" :loading="loading">
|
||||
<vxe-column type="seq" width="60" />
|
||||
<vxe-column field="handlerName" title="处理器名称" />
|
||||
<vxe-column field="className" title="处理器全限定名" />
|
||||
<vxe-column field="success" title="是否执行成功">
|
||||
<template #default="{ row }">
|
||||
<a-tag v-if="row.success" color="green">成功</a-tag>
|
||||
<a-tag v-else color="red">失败</a-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="errorMessage" title="错误信息" />
|
||||
<vxe-column field="startTime" title="开始时间" />
|
||||
<vxe-column field="endTime" title="结束时间" />
|
||||
<vxe-column field="duration" title="执行时长(毫秒)" />
|
||||
<vxe-column field="createTime" title="创建时间" />
|
||||
</vxe-table>
|
||||
<vxe-pager
|
||||
size="medium"
|
||||
:loading="loading"
|
||||
:current-page="pagination.current"
|
||||
:page-size="pagination.size"
|
||||
:total="pagination.total"
|
||||
@page-change="handleTableChange"
|
||||
/>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { $ref } from 'vue/macros'
|
||||
import { del, page } from './QuartzJobLog.api'
|
||||
import useTablePage from '/@/hooks/bootx/useTablePage'
|
||||
import { VxeTableInstance, VxeToolbarInstance } from 'vxe-table'
|
||||
import BQuery from '/@/components/Bootx/Query/BQuery.vue'
|
||||
import { FormEditType } from '/@/enums/formTypeEnum'
|
||||
import { useMessage } from '/@/hooks/web/useMessage'
|
||||
import { QueryField } from '/@/components/Bootx/Query/Query'
|
||||
import { QuartzJob } from './QuartzJob.api'
|
||||
|
||||
// 使用hooks
|
||||
const { handleTableChange, pageQueryResHandel, resetQueryParams, pagination, pages, model, loading } = useTablePage(queryPage)
|
||||
const { notification, createMessage } = useMessage()
|
||||
|
||||
// 查询条件
|
||||
const fields = [
|
||||
{
|
||||
name: '执行状态',
|
||||
placeholder: '请选择状态',
|
||||
field: 'success',
|
||||
type: 'list',
|
||||
md: 12,
|
||||
selectList: [
|
||||
{ label: '成功', value: 'true' },
|
||||
{ label: '失败', value: 'false' },
|
||||
],
|
||||
},
|
||||
] as QueryField[]
|
||||
let job: QuartzJob
|
||||
let visible = $ref(false)
|
||||
const xTable = $ref<VxeTableInstance>()
|
||||
const xToolbar = $ref<VxeToolbarInstance>()
|
||||
|
||||
nextTick(() => {
|
||||
xTable.connect(xToolbar)
|
||||
})
|
||||
|
||||
function init(quartzJob) {
|
||||
job = quartzJob
|
||||
visible = true
|
||||
queryPage()
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
function queryPage() {
|
||||
loading.value = true
|
||||
page({
|
||||
className: job.jobClassName,
|
||||
...model.queryParam,
|
||||
...pages,
|
||||
}).then(({ data }) => {
|
||||
pageQueryResHandel(data)
|
||||
})
|
||||
}
|
||||
|
||||
// 删除
|
||||
function remove(record) {
|
||||
del(record.id).then(() => {
|
||||
createMessage.success('删除成功')
|
||||
})
|
||||
queryPage()
|
||||
}
|
||||
defineExpose({
|
||||
init,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
@@ -32,7 +32,7 @@
|
||||
</a-popconfirm>
|
||||
<a-divider type="vertical" />
|
||||
<a-dropdown>
|
||||
<a class="ant-dropdown-link"> 授权 <Icon icon="ant-design:down-outlined" :size="12" /> </a>
|
||||
<a> 授权 <Icon icon="ant-design:down-outlined" :size="12" /> </a>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item>
|
||||
|
9
types/store.d.ts
vendored
9
types/store.d.ts
vendored
@@ -47,3 +47,12 @@ export interface BeforeMiniState {
|
||||
menuMode?: MenuModeEnum
|
||||
menuType?: MenuTypeEnum
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典
|
||||
*/
|
||||
export interface Dict {
|
||||
dictCode: string
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
12
yarn.lock
12
yarn.lock
@@ -3337,6 +3337,13 @@ create-require@^1.1.0:
|
||||
resolved "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
cron-parser@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.npmmirror.com/cron-parser/-/cron-parser-4.6.0.tgz#404c3fdbff10ae80eef6b709555d577ef2fd2e0d"
|
||||
integrity sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA==
|
||||
dependencies:
|
||||
luxon "^3.0.1"
|
||||
|
||||
cropperjs@^1.5.12:
|
||||
version "1.5.12"
|
||||
resolved "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.5.12.tgz#d9c0db2bfb8c0d769d51739e8f916bbc44e10f50"
|
||||
@@ -6250,6 +6257,11 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
luxon@^3.0.1:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmmirror.com/luxon/-/luxon-3.0.4.tgz#d179e4e9f05e092241e7044f64aaa54796b03929"
|
||||
integrity sha512-aV48rGUwP/Vydn8HT+5cdr26YYQiUZ42NM6ToMoaGKwYfWbfLeRkEu1wXWMHBZT6+KyLfcbbtVcoQFCbbPjKlw==
|
||||
|
||||
magic-string@^0.25.0, magic-string@^0.25.7:
|
||||
version "0.25.9"
|
||||
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
|
||||
|
Reference in New Issue
Block a user