mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-12 20:08:59 +08:00
feat(manager): 优化定时任务管理、敏感词场景配置与商品选择器多规格匹配
This commit is contained in:
@@ -1,11 +1,16 @@
|
|||||||
import { managerApiUrl, getRequest, postRequest } from '@/libs/axios';
|
import { managerApiUrl, getRequest, postRequest, putRequestWithNoForm } from '@/libs/axios';
|
||||||
|
|
||||||
// 获取定时任务列表
|
// 获取定时任务列表
|
||||||
export const getTimedTaskList = () => {
|
export const getTimedTaskList = () => {
|
||||||
return getRequest(`${managerApiUrl}/manager/setting/timed-task`);
|
return getRequest(`${managerApiUrl}/manager/setting/timed-task`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 手动触发定时任务
|
// 修改定时任务(Cron表达式、启用/暂停状态)
|
||||||
export const triggerTimedTask = (type) => {
|
export const updateTimedTask = (params) => {
|
||||||
return postRequest(`${managerApiUrl}/manager/setting/timed-task/trigger/${type}`);
|
return putRequestWithNoForm(`${managerApiUrl}/manager/setting/timed-task`, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手动触发定时任务
|
||||||
|
export const triggerTimedTask = (taskId) => {
|
||||||
|
return postRequest(`${managerApiUrl}/manager/setting/timed-task/trigger/${taskId}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ function syncTableSelection() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
goodsData.value.forEach((item) => {
|
goodsData.value.forEach((item) => {
|
||||||
const selected = selectedList.value.some((e) => e.id && e.id === item.id);
|
const selected = selectedList.value.some((e) => (e.id || e.skuId) && String(e.id || e.skuId) === String(item.id || item.skuId));
|
||||||
table.toggleRowSelection(item, selected);
|
table.toggleRowSelection(item, selected);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,105 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="operation">
|
<el-tabs v-model="activeTab" @tab-change="clickTab">
|
||||||
<el-button type="primary" @click="add">添加</el-button>
|
<el-tab-pane label="词库管理" name="words">
|
||||||
<el-button @click="delAll">批量删除</el-button>
|
<div class="operation">
|
||||||
</div>
|
<el-button type="primary" @click="add">添加</el-button>
|
||||||
|
<el-button @click="delAll">批量删除</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
ref="table"
|
ref="table"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
border
|
border
|
||||||
:data="data"
|
:data="data"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@selection-change="changeSelect"
|
@selection-change="changeSelect"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="60" align="center" />
|
<el-table-column type="selection" width="60" align="center" />
|
||||||
<el-table-column prop="sensitiveWord" label="敏感词" min-width="120" />
|
<el-table-column prop="sensitiveWord" label="敏感词" min-width="120" />
|
||||||
<el-table-column prop="createTime" label="创建时间" width="200" />
|
<el-table-column prop="createTime" label="创建时间" width="200" />
|
||||||
<el-table-column prop="updateTime" label="更新时间" width="200" />
|
<el-table-column prop="updateTime" label="更新时间" width="200" />
|
||||||
<el-table-column prop="createBy" label="操作人" min-width="150" />
|
<el-table-column prop="createBy" label="操作人" min-width="150" />
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row">
|
<template v-if="row">
|
||||||
<a class="link-text" @click="detail(row)">修改</a>
|
<a class="link-text" @click="detail(row)">修改</a>
|
||||||
<span class="op-split">|</span>
|
<span class="op-split">|</span>
|
||||||
<a class="link-text" @click="remove(row)">删除</a>
|
<a class="link-text" @click="remove(row)">删除</a>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="searchForm.pageNumber"
|
v-model:current-page="searchForm.pageNumber"
|
||||||
v-model:page-size="searchForm.pageSize"
|
v-model:page-size="searchForm.pageSize"
|
||||||
:page-sizes="[20, 50, 100]"
|
:page-sizes="[20, 50, 100]"
|
||||||
:total="total"
|
:total="total"
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
size="small"
|
size="small"
|
||||||
@current-change="changePage"
|
@current-change="changePage"
|
||||||
@size-change="changePageSize"
|
@size-change="changePageSize"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="敏感词设置" name="switch">
|
||||||
|
<div v-loading="settingLoading" class="switch-setting">
|
||||||
|
<ul class="side-menu">
|
||||||
|
<li
|
||||||
|
:class="{ active: sideKey === 'func' }"
|
||||||
|
@click="sideKey = 'func'"
|
||||||
|
>
|
||||||
|
功能开关
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
:class="{ active: sideKey === 'scene' }"
|
||||||
|
@click="sideKey = 'scene'"
|
||||||
|
>
|
||||||
|
应用场景配置
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="setting-content">
|
||||||
|
<template v-if="sideKey === 'func'">
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="row-label">敏感词过滤:</span>
|
||||||
|
<el-switch v-model="settingForm.enabled" />
|
||||||
|
</div>
|
||||||
|
<div class="row-desc">关闭后,所有场景的敏感词过滤均不生效</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-for="group in sensitiveGroups"
|
||||||
|
:key="group.key"
|
||||||
|
class="scene-row"
|
||||||
|
>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="row-label">{{ group.name }}:</span>
|
||||||
|
<el-switch
|
||||||
|
:model-value="!settingForm.disabledGroups.includes(group.key)"
|
||||||
|
@update:model-value="toggleGroup(group.key, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row-desc">涉及:{{ group.desc }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="save-bar">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="settingSaving"
|
||||||
|
@click="saveSetting"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@@ -72,11 +129,16 @@ import {
|
|||||||
delSensitive,
|
delSensitive,
|
||||||
insertSensitiveWords,
|
insertSensitiveWords,
|
||||||
updateSensitiveWords,
|
updateSensitiveWords,
|
||||||
|
getSetting,
|
||||||
|
setSetting,
|
||||||
} from "@/api/index";
|
} from "@/api/index";
|
||||||
import { Message, Modal } from "@/utils/message";
|
import { Message, Modal } from "@/utils/message";
|
||||||
|
|
||||||
defineOptions({ name: "sensitive-words" });
|
defineOptions({ name: "sensitive-words" });
|
||||||
|
|
||||||
|
const SETTING_KEY = "SENSITIVE_WORDS_SETTING";
|
||||||
|
|
||||||
|
const activeTab = ref("words");
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const modalType = ref(0);
|
const modalType = ref(0);
|
||||||
const modalVisible = ref(false);
|
const modalVisible = ref(false);
|
||||||
@@ -111,9 +173,90 @@ let id = null;
|
|||||||
const table = ref(null);
|
const table = ref(null);
|
||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
|
|
||||||
|
// 开关设置
|
||||||
|
const sideKey = ref("func");
|
||||||
|
const settingLoading = ref(false);
|
||||||
|
const settingSaving = ref(false);
|
||||||
|
const settingLoaded = ref(false);
|
||||||
|
const settingForm = reactive({
|
||||||
|
enabled: true,
|
||||||
|
disabledGroups: [],
|
||||||
|
});
|
||||||
|
const sensitiveGroups = [
|
||||||
|
{ key: "MEMBER", name: "会员信息", desc: "用户昵称、收货地址信息" },
|
||||||
|
{ key: "EVALUATION", name: "产品评价", desc: "用户发布评论信息" },
|
||||||
|
{ key: "STORE", name: "商家", desc: "商家入驻信息" },
|
||||||
|
{ key: "GOODS", name: "产品信息", desc: "商品发布、商品修改、商品分类信息" },
|
||||||
|
{ key: "PROMOTION", name: "促销活动", desc: "活动名称、优惠券名称信息" },
|
||||||
|
{ key: "DECORATION", name: "装修模块", desc: "导航名称、装修楼层中的标题和子标题" },
|
||||||
|
{ key: "REASON", name: "原因管理", desc: "退款/退货/换货原因信息" },
|
||||||
|
{ key: "ARTICLE", name: "资讯管理", desc: "分类名称、文章标题、文章详情" },
|
||||||
|
];
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
|
function clickTab(name) {
|
||||||
|
if (name === "switch" && !settingLoaded.value) {
|
||||||
|
loadSetting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function loadSetting() {
|
||||||
|
settingLoading.value = true;
|
||||||
|
getSetting(SETTING_KEY)
|
||||||
|
.then((res) => {
|
||||||
|
let parsed = {};
|
||||||
|
const raw = res ? res.result : null;
|
||||||
|
if (typeof raw === "string") {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
parsed = {};
|
||||||
|
}
|
||||||
|
} else if (raw && typeof raw === "object") {
|
||||||
|
parsed = raw;
|
||||||
|
}
|
||||||
|
settingForm.enabled =
|
||||||
|
parsed.enabled !== undefined ? Boolean(parsed.enabled) : true;
|
||||||
|
settingForm.disabledGroups = Array.isArray(parsed.disabledGroups)
|
||||||
|
? parsed.disabledGroups
|
||||||
|
: [];
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 设置记录不存在时按默认值处理
|
||||||
|
settingForm.enabled = true;
|
||||||
|
settingForm.disabledGroups = [];
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
settingLoading.value = false;
|
||||||
|
settingLoaded.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function toggleGroup(key, enabled) {
|
||||||
|
const index = settingForm.disabledGroups.indexOf(key);
|
||||||
|
if (enabled && index > -1) {
|
||||||
|
settingForm.disabledGroups.splice(index, 1);
|
||||||
|
} else if (!enabled && index === -1) {
|
||||||
|
settingForm.disabledGroups.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function saveSetting() {
|
||||||
|
settingSaving.value = true;
|
||||||
|
setSetting(SETTING_KEY, {
|
||||||
|
enabled: settingForm.enabled,
|
||||||
|
disabledGroups: settingForm.disabledGroups,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
Message.success("保存成功");
|
||||||
|
} else {
|
||||||
|
Message.error(res.message || "保存失败");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
settingSaving.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
function changePage(v) {
|
function changePage(v) {
|
||||||
searchForm.pageNumber = v;
|
searchForm.pageNumber = v;
|
||||||
getDataList();
|
getDataList();
|
||||||
@@ -236,3 +379,81 @@ onMounted(() => {
|
|||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.switch-setting {
|
||||||
|
display: flex;
|
||||||
|
min-height: 300px;
|
||||||
|
border: 1px solid #e8eaec;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-menu {
|
||||||
|
width: 140px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 0;
|
||||||
|
list-style: none;
|
||||||
|
border-right: 1px solid #e8eaec;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 10px 16px;
|
||||||
|
color: #2d8cf0;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #2d8cf0;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
border-left-color: #2d8cf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-label {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 80px;
|
||||||
|
margin-right: 12px;
|
||||||
|
color: #515a6e;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-desc {
|
||||||
|
margin: 8px 0 0 92px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-row {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #e8eaec;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-bar {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,54 +1,281 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="timed-task-page">
|
||||||
<el-card>
|
<el-card shadow="never">
|
||||||
<el-tabs v-model="activeTab">
|
<!-- 顶部统计与操作栏 -->
|
||||||
<el-tab-pane label="每日任务" name="每日任务" />
|
<div class="header-bar">
|
||||||
<el-tab-pane label="每小时任务" name="每小时" />
|
<div class="stats-info">
|
||||||
<el-tab-pane label="每分钟任务" name="每分钟" />
|
<span class="stat-item">全部任务:<b>{{ taskList.length }}</b></span>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<span class="stat-item text-success">运行中:<b>{{ runningCount }}</b></span>
|
||||||
|
<el-divider direction="vertical" />
|
||||||
|
<span class="stat-item text-muted">已暂停:<b>{{ pausedCount }}</b></span>
|
||||||
|
</div>
|
||||||
|
<div class="filter-actions">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索任务名称 / 标识"
|
||||||
|
clearable
|
||||||
|
style="width: 240px; margin-right: 12px"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
/>
|
||||||
|
<el-button :icon="Refresh" @click="loadData" :loading="loading">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分组 Tabs -->
|
||||||
|
<el-tabs v-model="activeGroup" style="margin-top: 12px">
|
||||||
|
<el-tab-pane label="全部" name="ALL" />
|
||||||
|
<el-tab-pane
|
||||||
|
v-for="g in groupList"
|
||||||
|
:key="g"
|
||||||
|
:label="g"
|
||||||
|
:name="g"
|
||||||
|
/>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="currentTasks" border style="width: 100%; margin-top: 12px">
|
<!-- 任务列表表格 -->
|
||||||
<el-table-column prop="name" label="任务名称" width="220" />
|
<el-table
|
||||||
<el-table-column prop="cron" label="Cron表达式" width="140" />
|
v-loading="loading"
|
||||||
<el-table-column label="操作" width="120" align="center">
|
:data="filteredTasks"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%; margin-top: 8px"
|
||||||
|
>
|
||||||
|
<el-table-column label="任务名称 / 标识" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="task-title-cell">
|
||||||
|
<span class="task-name">{{ row.name }}</span>
|
||||||
|
<span class="task-id">{{ row.id }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="group" label="分组" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small" effect="plain">{{ row.group || '其他' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="description" label="任务说明" min-width="240" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column label="Cron 表达式" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="cron-cell">
|
||||||
|
<code class="cron-tag">{{ row.cron }}</code>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:icon="Edit"
|
||||||
|
title="修改Cron"
|
||||||
|
@click="openEditDialog(row)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-switch
|
||||||
|
v-model="row.status"
|
||||||
|
:loading="row.statusLoading"
|
||||||
|
:active-value="true"
|
||||||
|
:inactive-value="false"
|
||||||
|
@change="(val) => handleStatusChange(row, val)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="lastExecTime" label="上次执行时间" width="170" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.lastExecTime || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="上次执行结果" width="150" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row.lastExecStatus === 1">
|
||||||
|
<el-tag type="success" size="small">
|
||||||
|
成功
|
||||||
|
<span v-if="row.lastExecCost != null">({{ row.lastExecCost }}ms)</span>
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="row.lastExecStatus === 0">
|
||||||
|
<el-tooltip
|
||||||
|
class="box-item"
|
||||||
|
effect="dark"
|
||||||
|
:content="row.lastExecError || '未知错误'"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-tag type="danger" size="small" style="cursor: pointer">
|
||||||
|
失败 ℹ
|
||||||
|
</el-tag>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-tag type="info" size="small">未执行</el-tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
@click="openEditDialog(row)"
|
||||||
|
>
|
||||||
|
配置
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
link
|
||||||
size="small"
|
size="small"
|
||||||
:loading="row.triggering"
|
:loading="row.triggering"
|
||||||
@click="handleTrigger(row)"
|
@click="handleTrigger(row)"
|
||||||
>
|
>
|
||||||
手动触发
|
立即执行
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 修改 Cron 表达式弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="editDialogVisible"
|
||||||
|
:title="`配置任务: ${currentRow?.name || ''}`"
|
||||||
|
width="540px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form label-width="110px" style="margin-top: 10px">
|
||||||
|
<el-form-item label="任务标识">
|
||||||
|
<el-input :model-value="currentRow?.id" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务名称">
|
||||||
|
<el-input :model-value="currentRow?.name" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务说明">
|
||||||
|
<div class="desc-text">{{ currentRow?.description || '-' }}</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Cron 表达式" required>
|
||||||
|
<el-input
|
||||||
|
v-model="editForm.cron"
|
||||||
|
placeholder="例如: 0 0/5 * * * ?"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="快捷预设">
|
||||||
|
<div class="cron-presets">
|
||||||
|
<el-button
|
||||||
|
v-for="p in cronPresets"
|
||||||
|
:key="p.cron"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
bg
|
||||||
|
@click="editForm.cron = p.cron"
|
||||||
|
>
|
||||||
|
{{ p.label }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用状态">
|
||||||
|
<el-switch v-model="editForm.status" />
|
||||||
|
<span style="margin-left: 10px; color: #909399; font-size: 13px">
|
||||||
|
{{ editForm.status ? '保存后自动开启调度' : '保存后暂停该任务调度' }}
|
||||||
|
</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { getTimedTaskList, triggerTimedTask } from "@/api/timedTask.js";
|
import { Search, Refresh, Edit } from '@element-plus/icons-vue';
|
||||||
|
import { getTimedTaskList, updateTimedTask, triggerTimedTask } from '@/api/timedTask.js';
|
||||||
|
|
||||||
defineOptions({ name: "timed-task-manage" });
|
defineOptions({ name: 'timed-task-manage' });
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const activeTab = ref("每日任务");
|
|
||||||
const taskList = ref([]);
|
const taskList = ref([]);
|
||||||
|
const activeGroup = ref('ALL');
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
|
||||||
const currentTasks = computed(() => {
|
// 对话框表单
|
||||||
return taskList.value.filter((t) => t.group === activeTab.value);
|
const editDialogVisible = ref(false);
|
||||||
|
const currentRow = ref(null);
|
||||||
|
const saving = ref(false);
|
||||||
|
const editForm = ref({
|
||||||
|
id: '',
|
||||||
|
cron: '',
|
||||||
|
status: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
function init() {
|
// 快捷 Cron 模板
|
||||||
|
const cronPresets = [
|
||||||
|
{ label: '每1分钟', cron: '0 * * * * ?' },
|
||||||
|
{ label: '每5分钟', cron: '0 0/5 * * * ?' },
|
||||||
|
{ label: '每10分钟', cron: '0 0/10 * * * ?' },
|
||||||
|
{ label: '每30分钟', cron: '0 0/30 * * * ?' },
|
||||||
|
{ label: '每小时整点', cron: '0 0 * * * ?' },
|
||||||
|
{ label: '每日0点', cron: '0 0 0 * * ?' },
|
||||||
|
{ label: '每日凌晨2点', cron: '0 0 2 * * ?' },
|
||||||
|
{ label: '每日凌晨3点', cron: '0 0 3 * * ?' },
|
||||||
|
{ label: '每日凌晨6点', cron: '0 0 6 * * ?' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const runningCount = computed(() => {
|
||||||
|
return taskList.value.filter((t) => t.status).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pausedCount = computed(() => {
|
||||||
|
return taskList.value.filter((t) => !t.status).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupList = computed(() => {
|
||||||
|
const set = new Set();
|
||||||
|
taskList.value.forEach((t) => {
|
||||||
|
if (t.group) set.add(t.group);
|
||||||
|
});
|
||||||
|
return Array.from(set);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredTasks = computed(() => {
|
||||||
|
let list = taskList.value;
|
||||||
|
if (activeGroup.value !== 'ALL') {
|
||||||
|
list = list.filter((t) => t.group === activeGroup.value);
|
||||||
|
}
|
||||||
|
if (searchKeyword.value.trim()) {
|
||||||
|
const kw = searchKeyword.value.trim().toLowerCase();
|
||||||
|
list = list.filter(
|
||||||
|
(t) =>
|
||||||
|
(t.name && t.name.toLowerCase().includes(kw)) ||
|
||||||
|
(t.id && t.id.toLowerCase().includes(kw)) ||
|
||||||
|
(t.description && t.description.toLowerCase().includes(kw))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
getTimedTaskList()
|
getTimedTaskList()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success) {
|
if (res.success && res.result) {
|
||||||
taskList.value = res.result.map((t) => ({ ...t, triggering: false }));
|
taskList.value = res.result.map((t) => ({
|
||||||
|
...t,
|
||||||
|
statusLoading: false,
|
||||||
|
triggering: false,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -56,11 +283,70 @@ function init() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStatusChange(row, newStatus) {
|
||||||
|
row.statusLoading = true;
|
||||||
|
updateTimedTask({
|
||||||
|
id: row.id,
|
||||||
|
status: newStatus,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
row.statusLoading = false;
|
||||||
|
if (res.success) {
|
||||||
|
ElMessage.success(`任务 "${row.name}" 已${newStatus ? '开启' : '暂停'}`);
|
||||||
|
} else {
|
||||||
|
row.status = !newStatus;
|
||||||
|
ElMessage.error(res.message || '修改状态失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
row.statusLoading = false;
|
||||||
|
row.status = !newStatus;
|
||||||
|
ElMessage.error('网络异常,修改状态失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(row) {
|
||||||
|
currentRow.value = row;
|
||||||
|
editForm.value = {
|
||||||
|
id: row.id,
|
||||||
|
cron: row.cron,
|
||||||
|
status: row.status,
|
||||||
|
};
|
||||||
|
editDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (!editForm.value.cron || !editForm.value.cron.trim()) {
|
||||||
|
ElMessage.warning('Cron 表达式不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saving.value = true;
|
||||||
|
updateTimedTask({
|
||||||
|
id: editForm.value.id,
|
||||||
|
cron: editForm.value.cron.trim(),
|
||||||
|
status: editForm.value.status,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
saving.value = false;
|
||||||
|
if (res.success) {
|
||||||
|
ElMessage.success('定时任务配置保存成功');
|
||||||
|
editDialogVisible.value = false;
|
||||||
|
loadData();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '保存失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
saving.value = false;
|
||||||
|
ElMessage.error('保存失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleTrigger(row) {
|
function handleTrigger(row) {
|
||||||
ElMessageBox.confirm(`确认要手动触发 "${row.name}" 吗?`, "提示", {
|
ElMessageBox.confirm(`确认要手动立即执行一次 "${row.name}" 任务吗?`, '提示', {
|
||||||
confirmButtonText: "确认",
|
confirmButtonText: '立即执行',
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: '取消',
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
row.triggering = true;
|
row.triggering = true;
|
||||||
@@ -68,20 +354,100 @@ function handleTrigger(row) {
|
|||||||
.then((res) => {
|
.then((res) => {
|
||||||
row.triggering = false;
|
row.triggering = false;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
ElMessage.success(`"${row.name}" 已触发执行`);
|
ElMessage.success(`"${row.name}" 执行成功`);
|
||||||
|
loadData();
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(res.message || "触发失败");
|
ElMessage.error(res.message || '执行失败');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((err) => {
|
||||||
row.triggering = false;
|
row.triggering = false;
|
||||||
ElMessage.error("触发失败");
|
ElMessage.error(err?.response?.data?.message || '执行失败');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
loadData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.timed-task-page {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item b {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-success b {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted b {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-tag {
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
color: #409eff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: Consolas, Monaco, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-text {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user