定时任务

This commit is contained in:
zhang zhuo 2025-10-23 11:54:03 +08:00
parent 40e8bcb05d
commit 47589c5d11
6 changed files with 359 additions and 234 deletions

View File

@ -118,5 +118,16 @@ export default {
option: async function (data = {}) {
return await http.get("crontab/option", data);
},
},
crontab_log: {
list: async function (data = {}) {
return await http.get("crontab_log/list", data);
},
del: async function (data = {}) {
return await http.delete("crontab_log/del", data);
},
empty: async function (data = {}) {
return await http.delete("crontab_log/remove_all", data);
},
}
}

View File

@ -1,35 +1,15 @@
<template>
<el-select ref="selectRef" v-model="defaultValue" :size="size" :clearable="clearable" :multiple="multiple"
:collapse-tags="collapseTags" :collapse-tags-tooltip="collapseTagsTooltip" :filterable="filterable"
:placeholder="placeholder" :disabled="disabled" :filter-method="filterMethod" @remove-tag="removeTag"
@visible-change="visibleChange" @clear="clear">
<template #empty>
<div class="pi-table-select__table" :style="{width: tableWidth+'px'}" v-loading="loading">
<div class="pi-table-select__header">
<slot name="header" :form="formData" :submit="formSubmit"></slot>
</div>
<el-table ref="tableRef" :data="tableData" :height="245" :highlight-current-row="!multiple"
@row-click="click" @select="select" @select-all="selectAll">
<el-table-column v-if="multiple" type="selection" width="45"></el-table-column>
<el-table-column v-else type="index" width="45">
<template #default="scope"><span>{{ scope.$index + (currentPage - 1) * pageSize + 1 }}</span>
</template>
</el-table-column>
<slot></slot>
</el-table>
<div class="pi-table-select__page">
<el-pagination size="small" background layout="prev, pager, next" :total="total" :page-size="pageSize"
v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
</div>
</div>
</template>
:placeholder="placeholder" :disabled="disabled">
<el-option v-for="(item, index) in tableData" :key="index" :label="item[defaultProps.label]"
:value="item[defaultProps.value]"></el-option>
</el-select>
</template>
<script setup>
import config from '@/config/select'
import {ref, watch, onMounted, nextTick} from 'vue'
import {ref, watch, onMounted} from 'vue'
const emit = defineEmits(['update:modelValue', 'change'])
@ -51,204 +31,40 @@ const props = defineProps({
collapseTags: {type: Boolean, default: false},
collapseTagsTooltip: {type: Boolean, default: false},
disabled: {type: Boolean, default: false},
tableWidth: {type: Number, default: 400},
mode: {type: String, default: "popover"},
props: {
type: Object, default: () => {
}
}
})
const tableRef = ref(null)
const selectRef = ref(null)
let loading = ref(false)
let keyword = ref(null)
let defaultValue = ref([])
let tableData = ref([])
let pageSize = ref(config.pageSize)
let total = ref(0)
let currentPage = ref(1)
let defaultProps = ref({
label: config.props.label,
value: config.props.value,
page: config.request.page,
pageSize: config.request.pageSize,
keyword: config.request.keyword,
fields: []
value: config.props.value
})
let formData = ref({})
let tableData = ref([])
watch(() => props.modelValue, () => {
defaultValue.value = props.modelValue
autoCurrentLabel()
}, {deep: true})
watch(defaultValue, () => {
emit('update:modelValue', defaultValue.value)
emit('change', defaultValue.value)
})
onMounted(() => {
getData()
defaultProps.value = Object.assign(defaultProps.value, props.props);
defaultValue.value = props.modelValue
autoCurrentLabel()
})
//
function visibleChange(visible) {
if (visible) {
currentPage.value = 1
keyword.value = null
formData.value = {}
getData()
} else {
autoCurrentLabel()
}
}
async function getData() {
loading.value = true;
var reqData = {
[defaultProps.value.page]: currentPage.value,
[defaultProps.value.pageSize]: pageSize.value,
[defaultProps.value.keyword]: keyword.value
}
Object.assign(reqData, props.params, formData.value)
var res = await props.apiObj(reqData);
var parseData = config.parseData(res)
tableData.value = parseData.rows;
total.value = parseData.total;
const res = await props.apiObj(props.params);
tableData.value = res.data;
loading.value = false;
//
nextTick(() => {
if (props.multiple) {
defaultValue.value.forEach(row => {
var setrow = tableData.value.filter(item => item[defaultProps.value.value] === row[defaultProps.value.value])
if (setrow.length > 0) {
tableRef.value.toggleRowSelection(setrow[0], true);
}
})
} else {
var setrow = tableData.value.filter(item => item[defaultProps.value.value] === defaultValue.value[defaultProps.value.value])
tableRef.value.setCurrentRow(setrow[0]);
}
tableRef.value.setScrollTop(0)
})
}
//
function formSubmit() {
currentPage.value = 1
keyword.value = null
getData()
}
//
function reload() {
getData()
}
//options
function autoCurrentLabel() {
// nextTick(() => {
// if (props.multiple) {
// selectRef.value.selected.forEach(item => {
// item.currentLabel = item.value[defaultProps.value.label]
// })
// } else {
// selectRef.value.selectedLabel = defaultValue.value[defaultProps.value.label]
// }
// })
}
//
function select(rows, row) {
var isSelect = rows.length && rows.indexOf(row) !== -1
if (isSelect) {
defaultValue.value.push(row)
} else {
defaultValue.value.splice(defaultValue.value.findIndex(item => item[defaultProps.value.value] == row[defaultProps.value.value]), 1)
}
autoCurrentLabel()
//
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
//
function selectAll(rows) {
var isAllSelect = rows.length > 0
if (isAllSelect) {
rows.forEach(row => {
var isHas = defaultValue.value.find(item => item[defaultProps.value.value] == row[defaultProps.value.value])
if (!isHas) {
defaultValue.value.push(row)
}
})
} else {
this.tableData.forEach(row => {
var isHas = defaultValue.value.find(item => item[defaultProps.value.value] == row[defaultProps.value.value])
if (isHas) {
defaultValue.value.splice(defaultValue.value.findIndex(item => item[defaultProps.value.value] == row[defaultProps.value.value]), 1)
}
})
}
autoCurrentLabel()
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
function click(row) {
if (props.multiple) {
//
} else {
defaultValue.value = row
selectRef.value.blur()
autoCurrentLabel()
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
}
//tags
function removeTag(tag) {
var row = findRowByKey(tag[defaultProps.value.value])
tableRef.value.toggleRowSelection(row, false);
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
//
function clear() {
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
//
function findRowByKey(value) {
return tableData.value.find(item => item[defaultProps.value.value] === value)
}
function filterMethod(k) {
if (!k) {
keyword.value = null;
return false;
}
keyword.value = k;
getData()
}
// select
function blur() {
selectRef.value.blur();
}
// select
function focus() {
selectRef.value.focus();
}
</script>
<style scoped>
.pi-table-select__table {
padding: 12px;
}
.pi-table-select__page {
padding-top: 12px;
}
</style>

View File

@ -0,0 +1,254 @@
<template>
<el-select ref="selectRef" v-model="defaultValue" :size="size" :clearable="clearable" :multiple="multiple"
:collapse-tags="collapseTags" :collapse-tags-tooltip="collapseTagsTooltip" :filterable="filterable"
:placeholder="placeholder" :disabled="disabled" :filter-method="filterMethod" @remove-tag="removeTag"
@visible-change="visibleChange" @clear="clear">
<template #empty>
<div class="pi-table-select__table" :style="{width: tableWidth+'px'}" v-loading="loading">
<div class="pi-table-select__header">
<slot name="header" :form="formData" :submit="formSubmit"></slot>
</div>
<el-table ref="tableRef" :data="tableData" :height="245" :highlight-current-row="!multiple"
@row-click="click" @select="select" @select-all="selectAll">
<el-table-column v-if="multiple" type="selection" width="45"></el-table-column>
<el-table-column v-else type="index" width="45">
<template #default="scope"><span>{{ scope.$index + (currentPage - 1) * pageSize + 1 }}</span>
</template>
</el-table-column>
<slot></slot>
</el-table>
<div class="pi-table-select__page">
<el-pagination size="small" background layout="prev, pager, next" :total="total" :page-size="pageSize"
v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
</div>
</div>
</template>
</el-select>
</template>
<script setup>
import config from '@/config/select'
import {ref, watch, onMounted, nextTick} from 'vue'
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
modelValue: null,
apiObj: {
type: Function, default: () => {
}
},
params: {
type: Object, default: () => {
}
},
placeholder: {type: String, default: "请选择"},
size: {type: String, default: "default"},
clearable: {type: Boolean, default: false},
multiple: {type: Boolean, default: false},
filterable: {type: Boolean, default: false},
collapseTags: {type: Boolean, default: false},
collapseTagsTooltip: {type: Boolean, default: false},
disabled: {type: Boolean, default: false},
tableWidth: {type: Number, default: 400},
mode: {type: String, default: "popover"},
props: {
type: Object, default: () => {
}
}
})
const tableRef = ref(null)
const selectRef = ref(null)
let loading = ref(false)
let keyword = ref(null)
let defaultValue = ref([])
let tableData = ref([])
let pageSize = ref(config.pageSize)
let total = ref(0)
let currentPage = ref(1)
let defaultProps = ref({
label: config.props.label,
value: config.props.value,
page: config.request.page,
pageSize: config.request.pageSize,
keyword: config.request.keyword,
fields: []
})
let formData = ref({})
watch(() => props.modelValue, () => {
defaultValue.value = props.modelValue
autoCurrentLabel()
}, {deep: true})
onMounted(() => {
defaultProps.value = Object.assign(defaultProps.value, props.props);
defaultValue.value = props.modelValue
autoCurrentLabel()
})
//
function visibleChange(visible) {
if (visible) {
currentPage.value = 1
keyword.value = null
formData.value = {}
getData()
} else {
autoCurrentLabel()
}
}
async function getData() {
loading.value = true;
var reqData = {
[defaultProps.value.page]: currentPage.value,
[defaultProps.value.pageSize]: pageSize.value,
[defaultProps.value.keyword]: keyword.value
}
Object.assign(reqData, props.params, formData.value)
var res = await props.apiObj(reqData);
var parseData = config.parseData(res)
tableData.value = parseData.rows;
total.value = parseData.total;
loading.value = false;
//
nextTick(() => {
if (props.multiple) {
defaultValue.value.forEach(row => {
var setrow = tableData.value.filter(item => item[defaultProps.value.value] === row[defaultProps.value.value])
if (setrow.length > 0) {
tableRef.value.toggleRowSelection(setrow[0], true);
}
})
} else {
var setrow = tableData.value.filter(item => item[defaultProps.value.value] === defaultValue.value[defaultProps.value.value])
tableRef.value.setCurrentRow(setrow[0]);
}
tableRef.value.setScrollTop(0)
})
}
//
function formSubmit() {
currentPage.value = 1
keyword.value = null
getData()
}
//
function reload() {
getData()
}
//options
function autoCurrentLabel() {
// nextTick(() => {
// if (props.multiple) {
// selectRef.value.selected.forEach(item => {
// item.currentLabel = item.value[defaultProps.value.label]
// })
// } else {
// selectRef.value.selectedLabel = defaultValue.value[defaultProps.value.label]
// }
// })
}
//
function select(rows, row) {
var isSelect = rows.length && rows.indexOf(row) !== -1
if (isSelect) {
defaultValue.value.push(row)
} else {
defaultValue.value.splice(defaultValue.value.findIndex(item => item[defaultProps.value.value] == row[defaultProps.value.value]), 1)
}
autoCurrentLabel()
//
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
//
function selectAll(rows) {
var isAllSelect = rows.length > 0
if (isAllSelect) {
rows.forEach(row => {
var isHas = defaultValue.value.find(item => item[defaultProps.value.value] == row[defaultProps.value.value])
if (!isHas) {
defaultValue.value.push(row)
}
})
} else {
this.tableData.forEach(row => {
var isHas = defaultValue.value.find(item => item[defaultProps.value.value] == row[defaultProps.value.value])
if (isHas) {
defaultValue.value.splice(defaultValue.value.findIndex(item => item[defaultProps.value.value] == row[defaultProps.value.value]), 1)
}
})
}
autoCurrentLabel()
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
function click(row) {
if (props.multiple) {
//
} else {
defaultValue.value = row
selectRef.value.blur()
autoCurrentLabel()
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
emit('change', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
}
//tags
function removeTag(tag) {
var row = findRowByKey(tag[defaultProps.value.value])
tableRef.value.toggleRowSelection(row, false);
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
//
function clear() {
emit('update:modelValue', config.filter(defaultValue.value, defaultProps.value.fields, props.multiple));
}
//
function findRowByKey(value) {
return tableData.value.find(item => item[defaultProps.value.value] === value)
}
function filterMethod(k) {
if (!k) {
keyword.value = null;
return false;
}
keyword.value = k;
getData()
}
// select
function blur() {
selectRef.value.blur();
}
// select
function focus() {
selectRef.value.focus();
}
</script>
<style scoped>
.pi-table-select__table {
padding: 12px;
}
.pi-table-select__page {
padding-top: 12px;
}
</style>

View File

@ -2,10 +2,12 @@
<pi-table ref="tableRef" :apiObj="api.system.crontab.list" @selection-change="selectionChange">
<template #do>
<el-button v-auth="'crontab:add'" type="primary" icon="el-icon-plus" @click="add"></el-button>
<el-button v-auth="'crontab:edit'" type="primary" icon="el-icon-edit" @click="edit()"
<el-button v-auth="'crontab:edit'" type="success" icon="el-icon-edit" @click="edit()"
:disabled="selection.length!==1"></el-button>
<el-button v-auth="'crontab:del'" type="danger" plain icon="el-icon-delete" :disabled="selection.length===0"
@click="batch_del"></el-button>
<el-button v-auth="'crontab:log:del'" type="info" plain icon="el-icon-operation"
@click="show_log()"></el-button>
</template>
<template #search>
<el-input v-model="search.crontab_name" placeholder="任务名称" clearable style="width: 200px;"
@ -28,19 +30,21 @@
</template>
</el-table-column>
<el-table-column label="备注" prop="memo"></el-table-column>
<el-table-column label="操作" fixed="right" align="right" width="170">
<el-table-column label="操作" fixed="right" align="right" width="120">
<template #default="scope">
<el-button-group>
<el-button text type="primary" size="small" @click="show(scope.row, scope.$index)">查看
</el-button>
<el-button v-auth="'post:edit'" text type="primary" size="small"
<el-button text type="primary" size="small" @click="show(scope.row, scope.$index)">查看</el-button>
<el-button v-auth="'crontab:edit'" text type="success" size="small"
@click="edit(scope.row, scope.$index)">编辑
</el-button>
<el-popconfirm title="确定删除吗?" @confirm="del(scope.row, scope.$index)">
<template #reference>
<el-button v-auth="'post:del'" text type="primary" size="small">删除</el-button>
<el-button v-auth="'crontab:del'" text type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
<el-button v-auth="'crontab:log:del'" text type="info" size="small"
@click="show_log(scope.row, scope.$index)">日志
</el-button>
</el-button-group>
</template>
</el-table-column>
@ -53,6 +57,7 @@
import saveDialog from './save'
import api from "@/api/index";
import {getCurrentInstance, nextTick, ref} from "vue";
import router from "@/router/index"
defineOptions({
name: "monitorCrontab"
@ -93,6 +98,15 @@ async function show(row) {
})
}
function show_log(row) {
router.push({
path: "/monitor/crontab_log",
query: {
crontab_id: row?.crontab_id
}
})
}
//
async function del(row) {
const loading = proxy.$loading();

View File

@ -7,9 +7,6 @@
<el-form-item label="调用方法" prop="callback">
<el-input type="text" v-model="form.callback" placeholder="请输入调用方法" clearable></el-input>
</el-form-item>
<el-form-item label="调用参数" prop="params">
<el-input type="textarea" v-model="form.params" placeholder="请输入调用参数" clearable></el-input>
</el-form-item>
<el-form-item label="cron表达式" prop="rule">
<pi-cron v-model="form.rule"></pi-cron>
</el-form-item>
@ -58,7 +55,6 @@ let form = ref({
enable: 0,
skip_log: 1,
rule: '',
params: '',
callback: ''
})
const rules = ref({
@ -68,9 +64,6 @@ const rules = ref({
rule:[
{required: true, message: '请填写cron表达式'}
],
params:[
{required: true, message: '请填写调用参数'}
],
callback:[
{required: true, message: '请填写调用方法'}
]

View File

@ -1,24 +1,30 @@
<template>
<pi-table ref="tableRef" :apiObj="api.system.online.list">
<pi-table ref="tableRef" :apiObj="api.system.crontab_log.list" @selection-change="selectionChange">
<template #do>
<el-input v-model="search.username" placeholder="用户名" clearable style="width: 200px;" @keyup.enter="upsearch"></el-input>
<el-button v-auth="'crontab:log:del'" type="danger" plain icon="el-icon-delete"
:disabled="selection.length===0" @click="batch_del">删除</el-button>
<el-button v-auth="'crontab:log:empty'" type="danger" plain icon="el-icon-refresh-right"
@click="batch_empty">清空</el-button>
</template>
<template #search>
<pi-select v-model="search.crontab_id" :api-obj="api.system.crontab.option" placeholder="任务名称" clearable
:props="{label: 'crontab_name', value:'crontab_id'}" style="width: 200px;"></pi-select>
<el-button type="primary" icon="el-icon-search" @click="upsearch"></el-button>
</template>
<el-table-column label="#" type="index" width="50"></el-table-column>
<el-table-column label="会话编号" prop="session_id"></el-table-column>
<el-table-column label="用户名" prop="username"></el-table-column>
<el-table-column label="部门名称" prop="account.dept.dept_name"></el-table-column>
<el-table-column label="主机" prop="ip"></el-table-column>
<el-table-column label="登录地点" prop="location"></el-table-column>
<el-table-column label="浏览器" prop="os.browser"></el-table-column>
<el-table-column label="操作系统" prop="os.os"></el-table-column>
<el-table-column label="登录时间" prop="online_time"></el-table-column>
<el-table-column type="selection" width="50"></el-table-column>
<el-table-column label="日志编号" prop="log_id"></el-table-column>
<el-table-column label="任务名称" prop="crontab_name"></el-table-column>
<el-table-column label="调用目标" prop="callback"></el-table-column>
<el-table-column label="执行结果" prop="result"></el-table-column>
<el-table-column label="执行状态" prop="status"></el-table-column>
<el-table-column label="执行耗时" prop="duration"></el-table-column>
<el-table-column label="执行时间" prop="create_time"></el-table-column>
<el-table-column label="操作" fixed="right" align="right" width="100">
<template #default="scope">
<el-button-group>
<el-popconfirm title="确定强制退出吗?" @confirm="quit(scope.row, scope.$index)">
<el-popconfirm title="确定删除吗?" @confirm="del(scope.row, scope.$index)">
<template #reference>
<el-button v-auth="'online:quit'" text type="primary" size="small">强退</el-button>
<el-button v-auth="'crontab:log:del'" text type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</el-button-group>
@ -30,29 +36,60 @@
<script setup>
import api from "@/api/index";
import {getCurrentInstance, ref} from "vue";
import piSelect from "@/components/piSelect"
import {useRoute} from 'vue-router'
defineOptions({
name: "monitorOnline"
name: "monitorCrontabLog"
})
const {proxy} = getCurrentInstance()
const tableRef = ref(null)
const route = useRoute()
let search = ref({
username: null
crontab_id: route.query.crontab_id ? parseInt(route.query.crontab_id) : null
})
//
async function quit(row) {
const loading = proxy.$loading();
const res = await api.system.online.quit({session_id: row.session_id});
tableRef.value.refresh()
loading.close();
proxy.$message.success(res.msg)
}
let selection = ref([])
//
function upsearch() {
tableRef.value.upData(search.value)
}
function selectionChange(e){
selection.value = e;
}
async function batch_empty() {
proxy.$confirm(`确定清空所有日志吗?`, '提示', {
type: 'warning'
}).then(async () => {
const loading = proxy.$loading();
const res = await api.system.crontab_log.empty(search.value);
tableRef.value.refresh()
loading.close();
proxy.$message.success(res.msg)
})
}
async function batch_del() {
proxy.$confirm(`确定删除选中的 ${selection.value.length} 项吗?如果删除项中含有子集将会被一并删除`, '提示', {
type: 'warning'
}).then(async () => {
const loading = proxy.$loading();
const res = await api.system.crontab_log.del({ids: selection.value.map(item => item.log_id).toString()});
tableRef.value.refresh()
loading.close();
proxy.$message.success(res.msg)
})
}
async function del(row) {
const loading = proxy.$loading();
const res = await api.system.crontab_log.del({ids: row.log_id});
tableRef.value.refresh()
loading.close();
proxy.$message.success(res.msg)
}
</script>