cool-admin-vue/cool/modules/base/views/task.vue

1134 lines
20 KiB
Vue
Raw Normal View History

2021-02-28 01:55:04 +08:00
<template>
<div class="system-task">
<el-row class="box scroller1" type="flex" :gutter="10">
<!-- 系统用户自定义已停止 -->
<el-col v-for="(item, index) in list" :key="index">
<div class="block" :class="[`_${item.key}`]">
<div class="header">
<!-- 图标 -->
<i class="icon" :class="item.icon"></i>
<!-- 标题 -->
<span class="label">{{ item.label }}</span>
<!-- 数量 -->
<span class="num">({{ item.pagination.total }})</span>
<span class="flex1"></span>
<!-- 操作按钮 -->
<ul class="op-btn">
<li v-if="item.loading">
<i class="el-icon-loading"></i>
</li>
<li
v-else
@click="refreshTask({ page: 1 })"
class="refresh-btn"
v-permission="perm.delete"
>
<i class="el-icon-refresh"></i>
<span>刷新</span>
</li>
<li @click="edit(item.params)" class="add-btn" v-permission="perm.add">
<i class="el-icon-plus"></i>
<span>添加</span>
</li>
</ul>
</div>
<div class="container scroller1">
<draggable
v-model="list[index].list"
v-bind="drag.options"
tag="ul"
:data-type="item.params.type"
:data-status="item.params.status"
@end="(e) => onDragEnd(e, item)"
>
<li
v-for="item2 in list[index].list"
:key="item2.id"
:data-id="item2.id"
@contextmenu.stop.prevent="openCM($event, item2)"
class="_drag"
>
<div class="h">
<span class="type _warning" v-show="item2.status === 0">
{{ item2.type | task_type }}
</span>
<span class="name">{{ item2.name }}</span>
</div>
<div class="remark">{{ item2.remark }}</div>
<div class="f">
<template v-if="item2.status">
<span class="date">{{ item2.nextRunTime || "..." }}</span>
<span class="start">进行中</span>
</template>
<template v-else>
<span>...</span>
<span class="stop">已停止</span>
</template>
</div>
<el-row type="flex" class="op">
<el-col v-if="item2.status === 0" @click.native="start(item2)">
<i class="el-icon-video-play"></i>
<span>开始</span>
</el-col>
<el-col
v-else
@click.native="stop(item2)"
v-permission="perm.stop"
>
<i class="el-icon-video-pause"></i>
<span>暂停</span>
</el-col>
<el-col
@click.native="edit(item2)"
v-permission="{
and: [perm.update, perm.info]
}"
>
<i class="el-icon-edit"></i>
<span>编辑</span>
</el-col>
<el-col @click.native="findLog(item2)" v-permission="perm.log">
<i class="el-icon-tickets"></i>
<span>查看日志</span>
</el-col>
</el-row>
</li>
<li v-if="list[index].list.length == 0">
<div class="empty">暂无数据</div>
</li>
</draggable>
<el-button
class="more"
type="text"
size="mini"
@click="moreTask(index, item)"
v-if="item.pagination.total >= item.pagination.size"
>查看更多</el-button
>
</div>
<div class="footer">
<button class="btn-add" @click="edit(item.params)" v-permission="perm.add">
<i class="el-icon-plus"></i>
</button>
</div>
</div>
</el-col>
<!-- 日志 -->
<el-col v-permission="perm.log">
<div class="block _log">
<div class="header">
<!-- 标题 -->
<span class="label">日志</span>
<!-- 数量 -->
<span class="num">({{ logs.pagination.total }})</span>
<span class="flex1"></span>
<!-- 是否异常 -->
<el-checkbox-group
class="status"
v-model="logs.filters.status"
@change="filterLog"
>
<el-checkbox :label="0">异常</el-checkbox>
</el-checkbox-group>
<!-- 操作按钮 -->
<ul class="op-btn">
<li @click="refreshLog({ page: 1 })">
<i class="el-icon-refresh"></i>
<span>刷新</span>
</li>
<li v-if="logs.current" class="_current-log" @click="allLog">
<span>{{ logs.current.name }}</span>
<i class="el-icon-close"></i>
</li>
</ul>
</div>
<div
class="container"
v-loading="logs.loading"
element-loading-text="拼命加载中"
>
<ul class="scroller1" v-infinite-scroll="moreLog">
<li
v-for="(item, index) in logs.list"
:key="index"
:class="{ _error: item.status == 0 }"
@click="expandLog(item)"
>
<div class="h">
<span class="name">{{ index + 1 }} · {{ item.taskName }}</span>
</div>
<div class="remark" :class="{ _ellipsis: !item._expand }">
{{ item.detail || "..." }}
</div>
<div class="f">
<span>执行时间{{ item.createTime }}</span>
</div>
</li>
<li v-if="logs.list.length == 0">
<div class="empty">暂无数据</div>
</li>
</ul>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import draggable from "vuedraggable";
import { checkPerm } from "cool/modules/base";
2021-02-28 01:55:04 +08:00
import { Form, ContextMenu } from "cl-admin-crud";
export default {
name: "system-task",
components: {
draggable
},
data() {
return {
list: [
{
key: "sys",
label: "系统任务",
icon: "el-icon-s-tools",
list: [],
loading: false,
params: {
type: 0,
status: 1
},
pagination: {
size: 10,
page: 1,
total: 0
}
},
{
key: "user",
label: "用户自定义任务",
icon: "el-icon-user-solid",
list: [],
loading: false,
params: {
type: 1,
status: 1
},
pagination: {
size: 10,
page: 1,
total: 0
}
},
{
key: "stop",
label: "已停止任务",
list: [],
loading: false,
params: {
type: null,
status: 0
},
pagination: {
size: 10,
page: 1,
total: 0
}
}
],
logs: {
loading: false,
list: [],
pagination: {
size: 10,
page: 1
},
params: {},
filters: {
status: []
},
current: null
},
drag: {
options: {
group: "Task",
animation: 300,
ghostClass: "Ghost",
dragClass: "Drag",
draggable: "._drag"
}
}
};
},
filters: {
task_type(i) {
return i === 0 ? "系统" : "用户";
}
},
mounted() {
this.refreshTask({ page: 1 });
},
computed: {
perm() {
return this.$service.system.task.permission;
}
},
methods: {
// 任务拖动
onDragEnd({ to, item }) {
const status = to.getAttribute("data-status");
const type = to.getAttribute("data-type");
const id = item.getAttribute("data-id");
if (status == 0) {
this.stop({ id });
}
if (status == 1) {
this.start({ id, type });
}
},
// 右键菜单
openCM(e, { id, status, type, name }) {
let menus = [
{
label: "立即执行",
perm: ["once"],
"suffix-icon": "el-icon-video-play",
callback: (e, close) => {
this.once({ id });
close();
}
},
{
label: "编辑",
perm: ["update", "info"],
"suffix-icon": "el-icon-edit",
callback: (e, close) => {
this.edit({ id, type });
close();
}
},
{
label: "删除",
perm: ["delete"],
"suffix-icon": "el-icon-delete",
callback: (e, close) => {
this.delete({ id });
close();
}
},
{
label: "查看日志",
perm: ["log"],
"suffix-icon": "el-icon-tickets",
callback: (e, close) => {
this.findLog({ id, name });
close();
}
}
];
if (status == 1) {
menus.splice(1, 0, {
label: "暂停",
perm: ["stop"],
"suffix-icon": "el-icon-video-pause",
callback: (e, close) => {
this.stop({ id, type });
close();
}
});
} else {
menus.splice(1, 0, {
label: "开始",
perm: ["start"],
"suffix-icon": "el-icon-video-play",
callback: (e, close) => {
this.start({ id, type });
close();
}
});
}
ContextMenu.open(e, {
list: menus.filter((e) => {
return checkPerm({
and: e.perm.map((a) => this.perm[a])
});
})
});
return false;
},
// 编辑任务
async edit(params) {
const { id, type } = params || {};
let info = {
type
};
if (id) {
info = await this.$service.system.task.info({ id });
}
if (info.every) {
info.every /= 1000;
}
if (!info.limit) {
info.limit = undefined;
}
const { setForm } = Form.open({
title: `编辑任务`,
width: "600px",
props: {
"label-width": "80px"
},
items: [
{
label: "名称",
prop: "name",
value: info.name,
component: {
name: "el-input",
attrs: {
placeholder: "请输入名称"
}
},
rules: {
required: true,
message: "名称不能为空"
}
},
{
label: "类型",
prop: "taskType",
value: info.taskType || 0,
component: {
name: "el-select",
options: [
{
label: "cron",
value: 0
},
{
label: "时间间隔",
value: 1
}
],
on: {
change: (v) => {
if (v == 0) {
setForm("limit", undefined);
setForm("every", undefined);
} else {
setForm("cron", undefined);
}
}
}
}
},
{
label: "cron",
prop: "cron",
hidden: ({ scope }) => {
return scope.taskType == 1;
},
value: info.cron,
component: {
name: "el-input",
attrs: {
placeholder: "* * * * * *"
}
},
rules: {
required: true,
message: "cron不能为空"
}
},
{
label: "次数",
prop: "limit",
value: info.limit,
hidden: ({ scope }) => {
return scope.taskType == 0;
},
component: {
name: "el-input-number",
props: {
min: 1,
max: 10000
}
}
},
{
label: "间隔(秒)",
prop: "every",
value: info.every,
hidden: ({ scope }) => {
return scope.taskType == 0;
},
component: {
name: "el-input-number",
props: {
min: 1,
max: 100000000
}
},
rules: {
required: true,
message: "执行间隔不能为空"
}
},
{
label: "service",
prop: "service",
value: info.service,
component: {
name: "el-input",
attrs: {
placeholder: "sys.test.add(params)"
}
}
},
{
label: "开始时间",
prop: "startDate",
value: info.startDate,
component: {
name: "el-date-picker",
props: {
type: "datetime"
}
}
},
{
label: "结束时间",
prop: "endDate",
value: info.endDate,
component: {
name: "el-date-picker",
props: {
type: "datetime"
}
}
},
{
label: "备注",
prop: "remark",
value: info.remark,
component: {
name: "el-input",
props: {
type: "textarea"
}
}
},
{
label: "状态",
prop: "status",
value: info.status === 0 ? 0 : 1,
component: {
name: "el-radio-group",
options: [
{
label: "停止",
value: 0
},
{
label: "运行",
value: 1
}
]
}
}
],
on: {
submit: (data, { close, done }) => {
if (!data.limit) {
data.limit = null;
}
this.$service.system.task[id ? "update" : "add"]({
...info,
...data,
every: data.every * 1000
})
.then(() => {
this.refreshTask();
this.$message.success("保存成功");
close();
})
.catch((err) => {
this.$message.error(err);
done();
});
}
}
});
},
// 删除任务
delete({ id }) {
this.$confirm("此操作将永久删除该任务,是否继续?", "提示", {
type: "warning"
})
.then(() => {
this.$service.system.task.delete({ ids: id }).then(() => {
this.refreshTask();
});
})
.catch(() => {});
},
// 开始任务
start({ id, type }) {
this.$service.system.task
.start({ id, type })
.then(() => {
this.refreshTask();
})
.catch((err) => {
this.$message.error(err);
});
},
// 停止任务
stop({ id }) {
this.$service.system.task
.stop({ id })
.then(() => {
this.refreshTask();
})
.catch((err) => {
this.$message.error(err);
});
},
// 任务执行一次
once({ id }) {
this.$service.system.task
.once({ id })
.then(() => {
this.refreshTask();
})
.catch((err) => {
this.$message.error(err);
});
},
expandLog(e) {
this.$set(e, "_expand", !e._expand);
},
// 刷新任务
refreshTask(params, options) {
const { index, more } = options || {};
const arr =
index === undefined || index === null ? this.list.map((e, i) => i) : [index];
arr.forEach(async (k) => {
let item = this.list[k];
Object.assign(item.params, {
...item.pagination,
...params
});
this.$set(item, "loading", true);
let res = await this.$service.system.task.page(item.params);
this.moreList(res, item);
if (!more) {
this.$el.querySelector(`.block._${item.key} .container`).scroll(0, 0);
}
item.loading = false;
});
},
// 更多任务
moreTask(index) {
this.refreshTask(null, { index, more: true });
},
// 刷新日志
async refreshLog(newParams, options) {
if (this.logs.loading) {
return false;
}
if (!checkPerm(this.perm.log)) {
return false;
}
const { params, pagination } = this.logs;
const { more } = options || {};
Object.assign(params, {
...pagination,
...newParams
});
this.logs.loading = true;
let res = await this.$service.system.task.log(params);
this.moreList(res, this.logs);
if (!more) {
this.$el.querySelector(".block._log .container ul").scroll(0, 0);
}
this.logs.loading = false;
},
// 更多日志
moreLog() {
this.refreshLog(null, { more: true });
},
// 查看任务对应的日志
findLog(e) {
this.logs.current = e;
this.refreshLog({ page: 1, id: e.id });
},
// 所有日志
allLog() {
this.logs.current = null;
this.refreshLog({ page: 1, id: null });
},
// 过滤日志
filterLog([v]) {
this.refreshLog({ page: 1, status: v === undefined ? 1 : 0 });
},
moreList(res, { list, pagination }) {
const { page, size } = res.pagination;
const len = res.list.length;
const max = list.length;
if (page == 1) {
list.splice(0, max, ...res.list);
} else {
let start = max - (max % size);
let end = start + len;
list.splice(start, end, ...res.list);
}
if (len == size) {
res.pagination.page += 1;
}
Object.assign(pagination, res.pagination);
return page != 1;
}
}
};
</script>
<style lang="scss" scoped>
.Ghost {
opacity: 0.7;
}
.Drag {
border: 1px dashed #000 !important;
background: #fff !important;
}
.system-task {
.box {
height: 100%;
overflow-x: auto;
.el-col {
height: 100%;
width: 413px;
}
}
.block {
height: 100%;
width: 400px;
.header {
display: flex;
align-items: center;
height: 40px;
background-color: #f0f0f0;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
position: relative;
top: 5px;
z-index: 1;
padding: 0 10px 5px 10px;
i {
font-size: 18px;
}
.label {
font-size: 12px;
margin: 0 5px;
letter-spacing: 0.5px;
}
.num {
font-size: 12px;
}
.flex1 {
flex: 1;
}
.op-btn {
display: flex;
li {
display: flex;
align-items: center;
list-style: none;
cursor: pointer;
padding: 2px 10px;
background-color: #fff;
border-radius: 3px;
margin-left: 5px;
&:hover {
background-color: #dedede;
color: #444;
}
i {
font-size: 13px;
margin-right: 2px;
}
span {
font-size: 12px;
}
}
}
}
.container {
max-height: calc(100% - 90px);
overflow-y: auto;
margin-bottom: 5px;
z-index: 2;
position: relative;
background-color: #f7f7f7;
ul {
li {
list-style: none;
background-color: #fff;
border-radius: 5px;
margin-bottom: 5px;
padding: 10px 15px;
font-size: 14px;
letter-spacing: 0.5px;
border: 1px solid #f7f7f7;
&:last-child {
margin-bottom: 0;
}
&._drag {
cursor: pointer;
}
&:hover {
.op {
height: 30px;
}
}
.h {
display: flex;
align-items: center;
font-size: 14px;
margin-bottom: 10px;
.type {
font-size: 12px;
border-radius: 3px;
padding: 1px 2px;
margin-right: 5px;
&._warning {
background-color: $color-warning;
color: #fff;
}
}
}
.remark {
font-size: 12px;
color: #666;
margin-bottom: 20px;
}
.empty {
text-align: center;
font-size: 13px;
color: #666;
margin: 10px 0;
}
.f {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
.date {
font-size: 12px;
color: #fff;
background-color: $color-primary;
border-radius: 2px;
margin-left: 40px;
padding: 1px 3px;
&::before {
content: "NEXT";
position: absolute;
left: 0;
top: 1px;
color: #222;
}
}
.start,
.stop {
display: flex;
align-items: center;
font-size: 12px;
margin-left: 30px;
position: relative;
&::before {
content: "";
display: block;
height: 6px;
width: 6px;
border-radius: 6px;
position: absolute;
left: -15px;
}
}
.start {
color: #67c23a;
&::before {
background-color: #67c23a;
}
}
.stop {
color: #f56c6c;
&::before {
background-color: #f56c6c;
}
}
}
.op {
height: 0;
margin-top: 15px;
transition: height 0.3s;
overflow: hidden;
.el-col {
height: 30px;
display: flex;
justify-content: center;
align-items: center;
&:hover {
background-color: #f7f7f7;
}
span {
font-size: 12px;
color: #666;
}
i {
font-size: 16px;
margin-right: 5px;
}
}
}
&._error {
background-color: $color-danger;
color: #fff;
.remark {
color: #fff !important;
}
}
}
}
.more {
display: block;
margin: 10px auto;
}
}
.footer {
height: 36px;
.btn-add {
height: 34px;
width: 100%;
border-radius: 3px;
border: 0;
background-color: #fff;
cursor: pointer;
i {
font-size: 16px;
color: #999;
}
&:hover {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
}
}
}
.block._stop {
.header {
.add-btn {
display: none;
}
}
.container {
max-height: calc(100% - 50px);
}
.footer {
display: none;
}
}
.block._log {
.header {
.status {
.el-checkbox {
margin-right: 10px;
}
}
.op-btn {
li {
display: flex;
align-items: center;
height: 20px;
&._current-log {
span {
display: block;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
i {
margin-left: 2px;
}
&:hover {
i {
color: $color-danger;
}
}
}
}
}
}
.container {
height: calc(100% - 50px);
max-height: calc(100% - 50px);
ul {
height: 100%;
li {
.remark {
color: #999;
&._ellipsis {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
.f {
font-size: 12px;
}
&:hover {
.remark {
color: #444;
}
}
}
}
}
}
}
</style>