This commit is contained in:
zhenorzz 2022-11-02 17:08:48 +08:00
parent 72a8143fc3
commit 2754e63af5
8 changed files with 566 additions and 4 deletions

View File

@ -437,3 +437,15 @@ export class ServerExecScript extends Request {
this.param = param
}
}
export class ServerRemoteCrontabList extends Request {
readonly url = '/server/getRemoteCrontabList'
readonly method = 'get'
public param: {
serverId: number
}
constructor(param: ServerRemoteCrontabList['param']) {
super()
this.param = param
}
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1667293457240" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2299" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M831.825 63.94H191.94c-70.692 0-128 57.308-128 128v639.885c0 70.692 57.308 128 128 128h639.885c70.692 0 128-57.308 128-128V191.94c0-70.692-57.308-128-128-128zM895.885 832a63.835 63.835 0 0 1-63.973 63.886H192.088c-17.112 0-33.27-6.575-45.372-18.676s-18.836-28.098-18.836-45.21V192a64.236 64.236 0 0 1 64.208-64.12h639.824A64.038 64.038 0 0 1 895.885 192V832z" p-id="2300"></path><path d="M791.998 351.852H536a31.97 31.97 0 0 0 0 63.94h256a31.97 31.97 0 0 0 0-63.94z m0 256.121H536a31.97 31.97 0 0 0 0 63.94h256a31.97 31.97 0 0 0 0-63.94z m-447.996-79.975c-61.856 0-111.986 50.144-111.986 111.985S282.16 751.97 344.002 751.97s111.985-50.144 111.985-111.986-50.13-111.985-111.985-111.985z m33.982 145.982a48.045 48.045 0 1 1 14.088-33.982 47.746 47.746 0 0 1-14.088 33.986z m39.412-376.586L311.999 402.787l-41.391-41.395a31.97 31.97 0 1 0-45.213 45.213l63.997 64.002a31.97 31.97 0 0 0 45.214 0l128-128a31.97 31.97 0 0 0-45.21-45.213z" p-id="2301"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1667293561564" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1016.128 812.992 891.648 100.096c-6.72-44.16-43.776-78.144-89.536-79.936L221.952 20.16C175.552 22.016 137.728 56.96 131.968 102.208l-124.16 710.848C3.136 827.328 0 842.304 0 858.112c0 80.512 65.536 145.728 146.24 145.728l731.456 0c80.704 0 146.304-65.216 146.304-145.728C1024 842.24 1020.8 827.328 1016.128 812.992zM402.24 384.384c0-60.288 49.152-109.312 109.76-109.312s109.696 49.024 109.696 109.312c40.448 0 73.152 32.64 73.152 72.96s-32.704 72.96-73.152 72.96L402.24 530.304c-40.32 0-73.088-32.64-73.088-72.96S361.92 384.384 402.24 384.384zM877.696 931.008 146.24 931.008c-40.256 0-73.088-32.64-73.088-72.896 0-40.128 32.832-72.832 73.088-72.832l731.456 0c40.32 0 73.152 32.704 73.152 72.832C950.848 898.368 918.016 931.008 877.696 931.008zM841.152 821.76c-20.224 0-36.544 16.32-36.544 36.352 0 20.16 16.384 36.416 36.544 36.416s36.544-16.256 36.544-36.416C877.696 838.08 861.312 821.76 841.152 821.76z" p-id="4448"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -142,8 +142,7 @@
"serverAgent": "Monitor",
"serverScript": "Script",
"serverProcess": "Process",
"template": "Template",
"crontab": "Crontab",
"serverCrontab": "Crontab UI",
"serverCron": "Cron",
"namespace": "Namespace",
"namespaceSetting": "NS setting",

View File

@ -142,8 +142,8 @@
"serverAgent": "服务器监控",
"serverScript": "执行脚本",
"serverProcess": "进程管理",
"serverCrontab": "Crontab UI",
"serverCron": "定时任务",
"crontab": "Crontab管理",
"namespace": "空间管理",
"namespaceSetting": "空间设置",
"roleSetting": "角色设置",

View File

@ -13,6 +13,7 @@
<el-input-number
v-model="password.length"
:min="1"
:max="40"
placeholder="Please enter the password length"
/>
<el-button type="primary" @click="createPassword">Gen</el-button>

View File

@ -111,7 +111,7 @@ export default <RouteRecordRaw[]>[
component: () => import('@/views/server/sftp/index.vue'),
meta: {
title: 'serverSFTP',
icon: 'ftp',
icon: 'sftpManage',
permissions: [permission.ShowSftpFilePage],
},
},
@ -135,6 +135,16 @@ export default <RouteRecordRaw[]>[
permissions: [permission.ShowServerProcessPage],
},
},
// {
// path: 'crontab',
// name: 'ServerCrontab',
// component: () => import('@/views/server/crontab.vue'),
// meta: {
// title: 'serverCrontab',
// icon: 'crontabManage',
// permissions: [permission.ShowServerProcessPage],
// },
// },
{
path: 'cron',
name: 'ServerCron',

View File

@ -0,0 +1,538 @@
<template>
<el-row class="app-container">
<el-row class="app-bar" type="flex" justify="space-between">
<el-col :span="8">
<el-select
v-model="serverId"
placeholder="Select server"
style="width: 160px"
filterable
@change="selectServer"
>
<el-option
v-for="server in serverOption"
:key="server.id"
:label="server.label"
:value="server.id"
/>
</el-select>
</el-col>
<el-col v-if="serverId !== ''" :span="16" style="text-align: right">
<el-button
:loading="tableLoading"
type="primary"
:icon="Plus"
@click="handleAdd"
>
New
</el-button>
<el-button
:loading="tableLoading"
type="primary"
:icon="Film"
@click="refresList"
>
Backup
</el-button>
<el-button
:loading="tableLoading"
type="warning"
:icon="Download"
@click="refresList"
>
Import
</el-button>
<el-button
:loading="tableLoading"
type="warning"
:icon="Upload"
@click="refresList"
>
Export
</el-button>
<el-button
:loading="tableLoading"
type="success"
:icon="Refresh"
@click="getRemoteCrontabList"
>
Get from crontab
</el-button>
<el-button
:loading="tableLoading"
type="success"
:icon="Document"
@click="refresList"
>
Save to crontab
</el-button>
</el-col>
</el-row>
<el-row class="app-table">
<el-table
v-loading="tableLoading"
height="100%"
highlight-current-row
:data="tablePage.list"
>
<el-table-column prop="id" label="ID" width="100" />
<el-table-column
prop="expression"
:label="$t('expression')"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
prop="description"
:label="$t('description')"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
prop="insertTime"
:label="$t('insertTime')"
width="155"
align="center"
/>
<el-table-column
prop="updateTime"
:label="$t('updateTime')"
width="155"
align="center"
/>
<el-table-column
prop="operation"
:label="$t('op')"
width="130"
align="center"
:fixed="$store.state.app.device === 'mobile' ? false : 'right'"
>
<template #default="scope">
<Button
type="primary"
:icon="Edit"
:permissions="[pms.EditCron]"
@click="handleEdit(scope.row)"
/>
<Button
type="danger"
:icon="Delete"
:permissions="[pms.DeleteCron]"
@click="handleRemove(scope.row)"
/>
</template>
</el-table-column>
</el-table>
</el-row>
<el-row type="flex" justify="end" class="app-page">
<el-pagination
:total="tablePage.total"
:page-size="pagination.rows"
background
layout="total, prev, pager, next"
@current-change="handlePageChange"
/>
</el-row>
<el-dialog
v-model="dialogVisible"
:fullscreen="$store.state.app.device === 'mobile'"
:title="$t('setting')"
>
<el-form
ref="form"
v-loading="formProps.loading"
:rules="formRules"
:model="formData"
label-width="120px"
:label-position="
$store.state.app.device === 'desktop' ? 'right' : 'top'
"
>
<el-form-item :label="$t('command')" prop="command">
<el-input v-model="formData.command" autocomplete="off" />
</el-form-item>
<el-form-item label="Quick Schedule">
<el-row style="width: 100%">
<el-button type="primary" @click="handleQuickSet('startup')">
Startup
</el-button>
<el-button type="primary" @click="handleQuickSet('hourly')">
Hourly
</el-button>
<el-button type="primary" @click="handleQuickSet('daily')">
Daily
</el-button>
<el-button type="primary" @click="handleQuickSet('weekly')">
Weekly
</el-button>
<el-button type="primary" @click="handleQuickSet('monthly')">
Monthly
</el-button>
<el-button type="primary" @click="handleQuickSet('yearly')">
Yearly
</el-button>
</el-row>
</el-form-item>
<el-form-item>
<el-row style="width: 100%" align="bottom">
<el-row style="width: 60px; margin-right: 10px">
<span>Minute</span>
<el-input v-model="formData.minute" />
</el-row>
<el-row style="width: 60px; margin-right: 10px">
<span>Hour</span>
<el-input v-model="formData.hour" />
</el-row>
<el-row style="width: 60px; margin-right: 10px">
<span>Day</span>
<el-input v-model="formData.day" />
</el-row>
<el-row style="width: 60px; margin-right: 10px">
<span>Month</span>
<el-input v-model="formData.month" />
</el-row>
<el-row style="width: 60px; margin-right: 10px">
<span>Week</span>
<el-input v-model="formData.week" />
</el-row>
<el-button
:disabled="
formData.minute == '' ||
formData.hour == '' ||
formData.day == '' ||
formData.month == '' ||
formData.week == ''
"
type="primary"
@click="handleSetTime"
>
Set
</el-button>
</el-row>
<span>{{ formProps.dateLocale }}</span>
</el-form-item>
<el-form-item :label="$t('expression')" prop="expression">
<el-input v-model="formData.expression" disabled />
</el-form-item>
<el-form-item :label="$t('description')">
<el-input v-model="formData.description" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">
{{ $t('cancel') }}
</el-button>
<el-button
:disabled="formProps.disabled"
type="primary"
@click="submit"
>
{{ $t('confirm') }}
</el-button>
</template>
</el-dialog>
</el-row>
</template>
<script lang="ts">
export default { name: 'ServerCron' }
</script>
<script lang="ts" setup>
import pms from '@/permission'
import Button from '@/components/Permission/Button.vue'
import {
Film,
Upload,
Download,
Document,
Refresh,
Plus,
Edit,
Delete,
} from '@element-plus/icons-vue'
import cronstrue from 'cronstrue/i18n'
import { ServerRemoteCrontabList, ServerOption } from '@/api/server'
import { CronList, CronAdd, CronEdit, CronRemove, CronData } from '@/api/cron'
import type { ElForm } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { locale, t } = useI18n({ useScope: 'global' })
const serverId = ref('')
const dialogVisible = ref(false)
const serverOption = ref<ServerOption['datagram']['list']>([])
const tableLoading = ref(false)
const tableData = ref<CronList['datagram']['list']>([])
const pagination = ref({ page: 1, rows: 20 })
const form = ref<InstanceType<typeof ElForm>>()
const tempFormData = {
id: 0,
serverId: 0,
name: '',
command: '/bin/sh /data/sh/hello.sh',
minute: '*/1',
hour: '*',
day: '*',
month: '*',
week: '*',
expression: '',
description: '',
}
const formData = ref(tempFormData)
const formProps = ref({
loading: false,
disabled: false,
dateLocale: '',
})
const formRules: InstanceType<typeof ElForm>['rules'] = {
expression: [
{
required: true,
validator: (_, value) => {
if (value.trim().split(/\s+/).length != 6) {
return new Error('6 parts are required.')
}
try {
cronstrue.toString(value)
return true
} catch (error) {
if (typeof error === 'string') {
return new Error(error)
} else if (error instanceof Error) {
return error
}
}
},
trigger: 'blur',
},
],
command: [{ required: true, message: 'Command required', trigger: 'blur' }],
}
getServerOption()
function selectServer() {
getList()
}
function getServerOption() {
new ServerOption().request().then((response) => {
serverOption.value = response.data.list
})
}
function getRemoteCrontabList() {
tableLoading.value = true
tableData.value = []
new ServerRemoteCrontabList({ serverId: Number(serverId.value) })
.request()
.then((response) => {
console.log(response)
})
.finally(() => {
tableLoading.value = false
})
}
function getList() {
tableLoading.value = true
tableData.value = []
new CronList({ serverId: Number(serverId.value) })
.request()
.then((response) => {
tableData.value = response.data.list
})
.finally(() => {
tableLoading.value = false
})
}
const tablePage = computed(() => {
let _tableData = tableData.value
return {
list: _tableData.slice(
(pagination.value.page - 1) * pagination.value.rows,
pagination.value.page * pagination.value.rows
),
total: _tableData.length,
}
})
function refresList() {
pagination.value.page = 1
getList()
}
function handleAdd() {
restoreFormData()
formData.value.serverId = Number(serverId.value)
dialogVisible.value = true
}
function handleEdit(data: CronData) {
formData.value = data
dialogVisible.value = true
}
function handleRemove(data: CronData) {
ElMessageBox.confirm(
t('serverPage.deleteTips', { name: data.command }),
t('tips'),
{
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning',
}
)
.then(() => {
new CronRemove({ id: data.id }).request().then(() => {
getList()
ElMessage.success('Success')
})
})
.catch(() => {
ElMessage.info('Cancel')
})
}
function handleQuickSet(schedule: string) {
switch (schedule) {
case 'startup':
formData.value.expression = `@reboot ${formData.value.command}`
formData.value.minute = ''
formData.value.hour = ''
formData.value.day = ''
formData.value.month = ''
formData.value.week = ''
onExpressionChange()
break
case 'hourly':
formData.value.minute = '0'
formData.value.hour = '*'
formData.value.day = '*'
formData.value.month = '*'
formData.value.week = '*'
handleSetTime()
break
case 'daily':
formData.value.minute = '0'
formData.value.hour = '0'
formData.value.day = '*'
formData.value.month = '*'
formData.value.week = '*'
handleSetTime()
break
case 'monthly':
formData.value.minute = '0'
formData.value.hour = '0'
formData.value.day = '1'
formData.value.month = '*'
formData.value.week = '*'
handleSetTime()
break
case 'weekly':
formData.value.minute = '0'
formData.value.hour = '0'
formData.value.day = '*'
formData.value.month = '*'
formData.value.week = '0'
handleSetTime()
break
case 'yearly':
formData.value.minute = '0'
formData.value.hour = '0'
formData.value.day = '1'
formData.value.month = '1'
formData.value.week = '*'
handleSetTime()
break
default:
break
}
}
function handleSetTime() {
formData.value.expression = `${formData.value.minute} ${formData.value.hour} ${formData.value.day} ${formData.value.month} ${formData.value.week} ${formData.value.command}`
onExpressionChange()
}
function onExpressionChange() {
if (formData.value.expression.startsWith('@reboot')) {
formProps.value.dateLocale = 'Startup'
} else {
formProps.value.dateLocale = cronstrue.toString(
`${formData.value.minute} ${formData.value.hour} ${formData.value.day} ${formData.value.month} ${formData.value.week}`,
{
use24HourTimeFormat: true,
locale: getLocale(),
}
)
}
}
function handlePageChange(val = 1) {
pagination.value.page = val
}
function submit() {
form.value?.validate((valid) => {
formData.value.expression = formData.value.expression.trim()
if (valid) {
if (formData.value.id === 0) {
add()
} else {
edit()
}
return Promise.resolve(true)
} else {
return Promise.reject(false)
}
})
}
function add() {
formProps.value.disabled = true
new CronAdd(formData.value)
.request()
.then(() => {
getList()
ElMessage.success('Success')
})
.finally(() => {
formProps.value.disabled = dialogVisible.value = false
})
}
function edit() {
formProps.value.disabled = true
new CronEdit(formData.value)
.request()
.then(() => {
getList()
ElMessage.success('Success')
})
.finally(() => {
formProps.value.disabled = dialogVisible.value = false
})
}
function getLocale() {
if (locale.value === 'zh-cn') {
return 'zh_CN'
}
return locale.value
}
function restoreFormData() {
formData.value = { ...tempFormData }
}
</script>
<style lang="scss" scoped>
@import '@/styles/mixin.scss';
.template-dialog {
padding-right: 10px;
height: 400px;
overflow-y: auto;
@include scrollBar();
}
</style>