feat: 计划作业 (#87)

This commit is contained in:
2024-02-02 14:05:39 +08:00
committed by GitHub
parent 473b0c26f2
commit 8293ec0297
70 changed files with 2171 additions and 14 deletions

View File

@ -0,0 +1,106 @@
/**
* 计划作业服务
* @module @/api/sys/job
*/
import config from '@/config'
import http from '@/utils/request'
export default {
/**
* 批量删除计划作业
*/
bulkDelete: {
url: `${config.API_URL}/api/sys/job/bulk.delete`,
name: `批量删除计划作业`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 创建计划作业
*/
create: {
url: `${config.API_URL}/api/sys/job/create`,
name: `创建计划作业`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 删除计划作业
*/
delete: {
url: `${config.API_URL}/api/sys/job/delete`,
name: `删除计划作业`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 计划作业是否存在
*/
exist: {
url: `${config.API_URL}/api/sys/job/exist`,
name: `计划作业是否存在`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 获取单个计划作业
*/
get: {
url: `${config.API_URL}/api/sys/job/get`,
name: `获取单个计划作业`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 分页查询计划作业
*/
pagedQuery: {
url: `${config.API_URL}/api/sys/job/paged.query`,
name: `分页查询计划作业`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 查询计划作业
*/
query: {
url: `${config.API_URL}/api/sys/job/query`,
name: `查询计划作业`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 启用/禁用作业
*/
setEnabled: {
url: `${config.API_URL}/api/sys/job/set.enabled`,
name: `启用/禁用作业`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
/**
* 更新计划作业
*/
update: {
url: `${config.API_URL}/api/sys/job/update`,
name: `更新计划作业`,
post: async function (data = {}, config = {}) {
return await http.post(this.url, data, config)
},
},
}

View File

@ -0,0 +1,10 @@
<template>
<svg t="1706693297583" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4279" width="128" height="128">
<path
d="M553 186.4v-84h204.8c22.5 0 41-18.4 41-41 0-22.5-18.4-41-41-41H266.2c-22.5 0-41 18.4-41 41 0 22.5 18.4 41 41 41H471v84C264 207 102.4 381.5 102.4 593.9c0 226.2 183.4 409.6 409.6 409.6s409.6-183.4 409.6-409.6c0-212.4-161.7-387-368.6-407.5z m-41 735.2c-180.7 0-327.7-147-327.7-327.7s147-327.7 327.7-327.7 327.7 147 327.7 327.7-147 327.7-327.7 327.7z"
p-id="4280"></path>
<path
d="M532.5 556.5V368.6c0-22.5-18.4-41-41-41s-41 18.4-41 41v204.8c0 0.3 0.1 0.5 0.1 0.7 0 2.4 0.3 4.9 0.7 7.3 0.2 0.9 0.5 1.7 0.8 2.6 0.5 1.7 0.9 3.5 1.6 5.2 0.2 0.5 0.5 1 0.8 1.5 2 4.2 4.5 8.2 8 11.6l173.8 173.8c15.9 15.9 42 15.9 57.9 0 15.9-15.9 15.9-42 0-57.9L532.5 556.5z"
p-id="4281"></path>
</svg>
</template>

View File

@ -52,4 +52,5 @@ export { default as LoginLog } from './LoginLog.vue'
export { default as ExLog } from './ExLog.vue'
export { default as Key } from './Key.vue'
export { default as OpenDoor } from './OpenDoor.vue'
export { default as Alert } from './Alert.vue'
export { default as Alert } from './Alert.vue'
export { default as ScheduledJob } from './ScheduledJob.vue'

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,7 @@ html {
height: 100%;
background-color: #f6f8f9;
font-size: 13px;
font-family: monospace;
}
a {

View File

@ -0,0 +1,229 @@
<template>
<el-container>
<el-header>
<div class="left-panel">
<na-search
:controls="[
{
type: 'input',
field: ['root', 'keywords'],
placeholder: $t('作业编号 / 作业名称'),
style: 'width:20rem',
},
{
type: 'select',
field: ['dy', 'status'],
options: Object.entries(this.$GLOBAL.enums.jobStatues).map((x) => {
return { value: x[0], label: x[1][1] }
}),
placeholder: $t('作业状态'),
style: 'width:15rem',
},
{
type: 'select',
field: ['dy', 'httpMethod'],
options: Object.entries(this.$GLOBAL.enums.httpMethods).map((x) => {
return { value: x[0], label: x[1][1] }
}),
placeholder: $t('请求方式'),
style: 'width:15rem',
},
{
type: 'select',
field: ['dy', 'enabled'],
options: [
{ label: '启用', value: true },
{ label: '禁用', value: false },
],
placeholder: '状态',
style: 'width:15rem',
},
]"
:vue="this"
@search="onSearch" />
</div>
<div class="right-panel">
<na-button-add :vue="this" />
<el-button :disabled="selection.length === 0" icon="el-icon-delete" plain type="danger" @click="batchDel"></el-button>
</div>
</el-header>
<el-main class="nopadding">
<sc-table
ref="table"
v-loading="loading"
:apiObj="$API.sys_job.pagedQuery"
:default-sort="{ prop: 'createdTime', order: 'descending' }"
:params="query"
remote-filter
remote-sort
stripe
@selection-change="
(items) => {
selection = items
}
">
<el-table-column type="selection"></el-table-column>
<el-table-column :label="$t('作业编号')" prop="id" width="150" />
<el-table-column :label="$t('作业名称')" prop="jobName" />
<na-col-indicator
:label="$t('作业状态')"
:options="[
{ text: '空闲', type: 'success', value: 'idle' },
{ text: '运行', type: 'warning', value: 'running' },
]"
prop="status"
width="100"></na-col-indicator>
<na-col-indicator
:label="$t('请求方式')"
:options="
Object.entries(this.$GLOBAL.enums.httpMethods).map((x) => {
return { value: x[0], text: x[1][1] }
})
"
prop="httpMethod" />
<el-table-column :label="$t('上次执行时间')" prop="lastExecTime" sortable="custom" />
<el-table-column :label="$t('上次执行状态')" prop="lastStatusCode" sortable="custom" />
<el-table-column :label="$t('下次执行时间')" prop="nextExecTime" sortable="custom" />
<el-table-column :label="$t('创建时间')" prop="createdTime" sortable="custom" />
<el-table-column :label="$t('启用')" prop="enabled">
<template #default="scope">
<el-switch v-model="scope.row.enabled" @change="changeSwitch($event, scope.row)"></el-switch>
</template>
</el-table-column>
<na-col-operation
:buttons="
naColOperation.buttons.concat({
icon: 'el-icon-delete',
confirm: true,
title: $t('删除作业'),
click: rowDel,
})
"
:vue="this" />
</sc-table>
</el-main>
</el-container>
<save-dialog
v-if="dialog.save"
ref="saveDialog"
@closed="dialog.save = false"
@success="(data, mode) => table.handleUpdate($refs.table, data, mode)"></save-dialog>
</template>
<script>
import saveDialog from './save'
import table from '@/config/table'
import naColOperation from '@/config/naColOperation'
export default {
components: {
saveDialog,
},
data() {
return {
loading: false,
query: {
dynamicFilter: {
filters: [],
},
filter: {},
},
dialog: {
save: false,
},
selection: [],
}
},
watch: {},
computed: {
naColOperation() {
return naColOperation
},
table() {
return table
},
},
mounted() {},
created() {},
methods: {
//表格内开关事件
async changeSwitch(event, row) {
try {
await this.$API.sys_job.setEnabled.post(row)
this.$message.success(`操作成功`)
} catch {
//
}
this.$refs.table.refresh()
},
//删除
async rowDel(row) {
try {
const res = await this.$API.sys_job.delete.post({ id: row.id })
this.$refs.table.refresh()
this.$message.success(`删除 ${res.data}`)
} catch {
//
}
},
//批量删除
async batchDel() {
let loading
try {
await this.$confirm(`确定删除选中的 ${this.selection.length} 项吗?`, '提示', {
type: 'warning',
})
loading = this.$loading()
const res = await this.$API.sys_job.bulkDelete.post({
items: this.selection,
})
this.$refs.table.refresh()
this.$message.success(`删除 ${res.data}`)
} catch {
//
}
loading?.close()
},
//搜索
onSearch(form) {
if (Array.isArray(form.dy.createdTime)) {
this.query.dynamicFilter.filters.push({
field: 'createdTime',
operator: 'dateRange',
value: form.dy.createdTime,
})
}
if (typeof form.dy.status === 'string' && form.dy.status.trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'status',
operator: 'eq',
value: form.dy.status,
})
}
if (typeof form.dy.enabled === 'boolean') {
this.query.dynamicFilter.filters.push({
field: 'enabled',
operator: 'eq',
value: form.dy.enabled,
})
}
if (typeof form.dy.httpMethod === 'string' && form.dy.httpMethod.trim() !== '') {
this.query.dynamicFilter.filters.push({
field: 'httpMethod',
operator: 'eq',
value: form.dy.httpMethod,
})
}
this.$refs.table.upData()
},
},
}
</script>
<style scoped></style>

View File

@ -0,0 +1,193 @@
<template>
<sc-dialog v-model="visible" :title="titleMap[mode]" :width="800" destroy-on-close fullscreen @closed="$emit('closed')">
<el-form
ref="dialogForm"
v-loading="loading"
:disabled="mode === 'view'"
:model="form"
:rules="rules"
label-position="right"
label-width="150px">
<el-tabs tab-position="top">
<el-tab-pane :label="$t('基本信息')">
<el-form-item v-if="mode === 'view'" :label="$t('作业编号')" prop="id">
<el-input v-model="form.id" clearable />
</el-form-item>
<el-form-item :label="$t('执行计划')" prop="executionCron">
<el-input v-model="form.executionCron" clearable />
</el-form-item>
<el-form-item :label="$t('请求方法')" prop="httpMethod">
<el-select v-model="form.httpMethod" clearable filterable>
<el-option v-for="(item, i) in $GLOBAL.enums.httpMethods" :key="i" :label="item[1]" :value="i" />
</el-select>
</el-form-item>
<el-form-item :label="$t('作业名称')" prop="jobName">
<el-input v-model="form.jobName" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('最近一次执行时间')" prop="lastExecTime">
<el-input v-model="form.lastExecTime" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('下一次执行时间')" prop="nextExecTime">
<el-input v-model="form.nextExecTime" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('下一次执行时间编号')" prop="nextTimeId">
<el-input v-model="form.nextTimeId" clearable />
</el-form-item>
<el-form-item :label="$t('请求头')" prop="requestHeader">
<el-input v-model="form.requestHeader" clearable rows="5" type="textarea" />
</el-form-item>
<el-form-item :label="$t('请求体')" prop="requestBody">
<el-input v-model="form.requestBody" clearable rows="5" type="textarea" />
</el-form-item>
<el-form-item :label="$t('请求的网络地址')" prop="requestUrl">
<el-input v-model="form.requestUrl" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('作业状态')" prop="status">
<el-input v-model="form.status" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('执行用户编号')" prop="userId">
<el-input v-model="form.userId" clearable />
</el-form-item>
<el-form-item v-else :label="$t('执行用户')" prop="userId">
<na-user v-model="form.userId"></na-user>
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('创建时间')" prop="createdTime">
<el-input v-model="form.createdTime" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('修改时间')" prop="modifiedTime">
<el-input v-model="form.modifiedTime" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('数据版本')" prop="version">
<el-input v-model="form.version" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('创建者编号')" prop="createdUserId">
<el-input v-model="form.createdUserId" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('创建者用户名')" prop="createdUserName">
<el-input v-model="form.createdUserName" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('修改者编号')" prop="modifiedUserId">
<el-input v-model="form.modifiedUserId" clearable />
</el-form-item>
<el-form-item v-if="mode === 'view'" :label="$t('修改者用户名')" prop="modifiedUserName">
<el-input v-model="form.modifiedUserName" clearable />
</el-form-item>
</el-tab-pane>
<el-tab-pane v-if="mode === 'view'" :label="$t('原始数据')">
<json-viewer
:expand-depth="5"
:expanded="true"
:theme="this.$TOOL.data.get('APP_DARK') ? 'dark' : 'light'"
:value="form"
copyable
sort></json-viewer>
</el-tab-pane>
</el-tabs>
</el-form>
<template #footer>
<el-button @click="visible = false"> </el-button>
<el-button v-if="mode !== 'view'" :loading="loading" type="primary" @click="submit"> </el-button>
</template>
</sc-dialog>
</template>
<script>
import scEditor from '@/components/scEditor/index.vue'
export default {
components: {
scEditor,
},
emits: ['success', 'closed'],
data() {
return {
mode: 'add',
titleMap: {
view: this.$t('查看作业'),
add: this.$t('新增作业'),
edit: this.$t('编辑作业'),
},
visible: false,
loading: false,
//表单数据
form: {},
//验证规则
rules: {
executionCron: [
{
required: true,
pattern: this.$GLOBAL.chars.RGX_CRON,
message: this.$t('执行计划不正确'),
},
],
httpMethod: [
{
required: true,
message: this.$t('请求方不能为空'),
},
],
jobName: [
{
required: true,
message: this.$t('作业名称不能为空'),
},
],
requestUrl: [
{
required: true,
pattern: this.$GLOBAL.chars.RGX_URL,
message: this.$t('请求的网络地址不正确'),
},
],
userId: [
{
required: true,
trigger: 'blur',
message: this.$t('执行用户不能为空'),
validator: (rule, value, callback) => {
if (value.id) callback()
else callback(new Error())
},
},
],
},
}
},
mounted() {},
methods: {
//显示
async open(mode = 'add', data) {
this.visible = true
this.loading = true
this.mode = mode
if (data) {
const res = await this.$API.sys_job.get.post({ id: data.id })
Object.assign(this.form, res.data)
}
this.loading = false
return this
},
//表单提交方法
async submit() {
const valid = await this.$refs.dialogForm.validate().catch(() => {})
if (!valid) {
return false
}
try {
const method = this.mode === 'add' ? this.$API.sys_job.create : this.$API.sys_job.update
this.loading = true
const res = await method.post(Object.assign({}, this.form, { userId: this.form.userId.id }))
this.loading = false
this.$emit('success', res.data, this.mode)
this.visible = false
this.$message.success('操作成功')
} catch {
//
this.loading = false
}
},
},
}
</script>