feat 新增 定时任务管理, 字典hooks,

This commit is contained in:
xxm
2022-10-20 20:32:10 +08:00
parent 56edd85bd9
commit 3422d10244
37 changed files with 2101 additions and 34 deletions

View File

@@ -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",

View File

@@ -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
}

View 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>

View 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>

View 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>

View 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.

View 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,
}

View 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;
}
}

View 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;
}
}
}

View 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'

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
}
}

View 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

View File

@@ -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)

View File

@@ -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,
}
}

View File

@@ -1,5 +1,5 @@
import { FormEditType } from '/@/enums/formTypeEnum'
import { unref } from "vue";
import { unref } from 'vue'
/**
* 服务器校验

View File

@@ -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
View 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)
}

View File

@@ -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="操作">

View File

@@ -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>

View File

@@ -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,
})

View File

@@ -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'] }],

View File

@@ -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>

View 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
}

View 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>

View 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>

View 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
}

View 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>

View File

@@ -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
View File

@@ -47,3 +47,12 @@ export interface BeforeMiniState {
menuMode?: MenuModeEnum
menuType?: MenuTypeEnum
}
/**
* 字典
*/
export interface Dict {
dictCode: string
code: string
name: string
}

View File

@@ -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"