任务模块

This commit is contained in:
yuhawu 2025-07-15 18:05:11 +08:00
parent 5e38aa11b6
commit cbaa331831
6 changed files with 370 additions and 78 deletions

View File

@ -1,11 +1,27 @@
//响应拦截器 //响应拦截器
import axios from 'axios'; // 引入axios用于创建新请求 import axios from 'axios'; // 引入axios用于创建新请求
import { ElMessage } from 'element-plus'; // 引入Element Plus的消息组件
import store from '../../store'; // 引入store用于统一处理登出
export function setupResponseInterceptors(instance) { export function setupResponseInterceptors(instance) {
// 用于存储正在刷新token的Promise // 用于存储正在刷新token的Promise
let isRefreshing = false; let isRefreshing = false;
// 存储等待token刷新的请求队列 // 存储等待token刷新的请求队列
let requests = []; let requests = [];
// 统一处理登录过期的函数
const handleTokenExpired = (message = '登录已过期,请重新登录') => {
// 显示提示消息
ElMessage.error(message);
// 使用store的logout action清除认证状态
store.dispatch('logout');
// 延迟跳转,让用户有时间看到提示
setTimeout(() => {
window.location.href = '/login';
}, 1500);
};
instance.interceptors.response.use( instance.interceptors.response.use(
response => { response => {
@ -20,70 +36,110 @@ export function setupResponseInterceptors(instance) {
return data; return data;
}, },
async error => { async error => {
console.log('响应错误:', error);
// 获取原始请求配置 // 获取原始请求配置
const originalRequest = error.config; const originalRequest = error.config;
// 判断是否401错误token过期且没有在刷新中 // 检查是否有响应
if (error.response?.status === 401 && !originalRequest._retry) { if (!error.response) {
// 标记该请求已尝试过重试 // 网络错误或请求被中断
originalRequest._retry = true; return Promise.reject(error);
}
// 如果当前没有在刷新token
if (!isRefreshing) { // 检查错误响应中的各种可能的错误信息位置
isRefreshing = true; const errorResponse = error.response;
console.log('错误响应:', errorResponse);
const errorData = errorResponse.data || {};
const errorMessage = errorData.message || errorData.error || errorResponse.statusText || '';
const errorStack = errorData.stack || '';
const statusCode = errorResponse.status;
// 处理403错误权限不足
if (errorResponse.status === "403") {
// 显示权限不足提示,但不重定向
ElMessage.error(errorMessage || '您没有权限执行此操作');
return Promise.reject("您没有权限执行此操作");
}
// 检查是否为令牌错误(检查多种可能的错误信息格式)
const isTokenError =
statusCode === 401 ||
(statusCode === 500 && (
errorMessage.includes('令牌') ||
errorMessage.includes('token') ||
errorMessage.includes('Token') ||
errorStack.includes('TokenIllegal') ||
errorStack.includes('TheTokenIllegalException') ||
(typeof errorData === 'string' && errorData.includes('令牌'))
));
// 如果是令牌错误,尝试刷新令牌或直接跳转登录页
if (isTokenError) {
// 如果是401错误且没有在刷新中尝试刷新令牌
if (statusCode === 401 && !originalRequest._retry) {
// 标记该请求已尝试过重试
originalRequest._retry = true;
try { // 如果当前没有在刷新token
// 从localStorage获取refreshToken if (!isRefreshing) {
const refreshToken = localStorage.getItem('refreshToken'); isRefreshing = true;
if (!refreshToken) { try {
// 无refreshToken跳转到登录页 // 从localStorage获取refreshToken
window.location.href = '/login'; const refreshToken = localStorage.getItem('refreshToken');
return Promise.reject(error);
if (!refreshToken) {
// 无refreshToken跳转到登录页
handleTokenExpired();
return Promise.reject(error);
}
// 创建表单数据
const formData = new FormData();
formData.append('refreshToken', refreshToken);
// 调用刷新token接口
const response = await axios.post('/admin/getAccessToken', formData);
// 获取新的token
const { accessToken, refreshToken: newRefreshToken } = response.data.data;
// 更新本地存储
localStorage.setItem('accessToken', accessToken);
if (newRefreshToken) {
localStorage.setItem('refreshToken', newRefreshToken);
}
// 更新当前请求的Authorization头
originalRequest.headers.Authorization = accessToken;
// 执行队列中的所有请求
requests.forEach(cb => cb(accessToken));
requests = [];
// 重新发送之前失败的请求
return instance(originalRequest);
} catch (refreshError) {
// 刷新token失败清除token并跳转到登录页
handleTokenExpired();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
} }
} else {
// 创建表单数据 // 如果已经在刷新中,将请求加入队列
const formData = new FormData(); return new Promise(resolve => {
formData.append('refreshToken', refreshToken); requests.push(token => {
originalRequest.headers.Authorization = token;
// 调用刷新token接口 resolve(instance(originalRequest));
const response = await axios.post('/admin/getAccessToken', formData); });
});
// 获取新的token
const { accessToken, refreshToken: newRefreshToken } = response.data.data;
// 更新本地存储
localStorage.setItem('accessToken', accessToken);
if (newRefreshToken) {
localStorage.setItem('refreshToken', newRefreshToken);
}
// 更新当前请求的Authorization头
originalRequest.headers.Authorization = accessToken;
// 执行队列中的所有请求
requests.forEach(cb => cb(accessToken));
requests = [];
// 重新发送之前失败的请求
return instance(originalRequest);
} catch (refreshError) {
// 刷新token失败清除token并跳转到登录页
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
} }
} else { } else {
// 如果已经在刷新中,将请求加入队列 // 其他令牌错误,直接处理登录过期
return new Promise(resolve => { handleTokenExpired();
requests.push(token => { return Promise.reject(error);
originalRequest.headers.Authorization = token;
resolve(instance(originalRequest));
});
});
} }
} }

13
src/api/modules/task.js Normal file
View File

@ -0,0 +1,13 @@
import instance from '../../utils/axios.js'
// 任务相关API
const taskApi = {
// 获取运行中的任务列表
getRunningTasks: () => instance.get('/task/test'),
// 停止指定任务
stopTask: (taskId) => instance.get(`/task/stopTask?taskId=${taskId}`),
};
// 导出模块
export { taskApi };

View File

@ -57,10 +57,6 @@
{ {
title: '权限管理', title: '权限管理',
path: '/user/permission' path: '/user/permission'
},
{
title: '用户角色管理',
path: '/user/userRole'
} }
] ]
}, },
@ -164,6 +160,19 @@
}] }]
}] }]
}, },
{
title: '任务管理',
path: '/task',
icon: TrendCharts,
children:[{
title: '任务列表',
path: '/task/list',
children:[{
title: '任务列表',
path: '/task/list'
}]
}]
},
{ {
title: '功能模块', title: '功能模块',
path: '/useModule', path: '/useModule',

View File

@ -28,11 +28,6 @@ const routes = [{
component: () => import('@/views/User/List.vue'), component: () => import('@/views/User/List.vue'),
meta: { title: '用户列表' } meta: { title: '用户列表' }
}, },
{
path: '/user/edit',
component: () => import('@/views/User/Edit.vue'),
meta: { title: '新增用户' }
},
{ {
path: '/user/role', path: '/user/role',
component: () => import('@/views/User/Role.vue'), component: () => import('@/views/User/Role.vue'),
@ -43,11 +38,6 @@ const routes = [{
component: () => import('@/views/User/Permission.vue'), component: () => import('@/views/User/Permission.vue'),
meta: { title: '权限管理' } meta: { title: '权限管理' }
}, },
{
path: '/user/userRole',
component: () => import('@/views/User/UserRole.vue'),
meta: { title: '用户角色管理' }
},
{ {
path: '/shop/list', path: '/shop/list',
component: () => import('@/views/Shop/index.vue'), component: () => import('@/views/Shop/index.vue'),
@ -83,11 +73,6 @@ const routes = [{
component: () => import('@/views/order/wechat/list.vue'), component: () => import('@/views/order/wechat/list.vue'),
meta: { title: '订单列表' } meta: { title: '订单列表' }
}, },
{
path: '/websocket/demo',
component: () => import('@/views/websocket/index.vue'),
meta: { title: 'WebSocket演示' }
},
{ {
path: '/book/selection/center', path: '/book/selection/center',
component: () => import('@/views/baseInfo/index.vue'), component: () => import('@/views/baseInfo/index.vue'),
@ -97,6 +82,11 @@ const routes = [{
path: '/warehouse/depot/list', path: '/warehouse/depot/list',
component: () => import('@/views/Warehouse/Depot/List.vue'), component: () => import('@/views/Warehouse/Depot/List.vue'),
meta: { title: '货区管理' } meta: { title: '货区管理' }
},
{
path: '/task/list',
component: () => import('@/views/Task/List.vue'),
meta: { title: '任务列表' }
} }
] ]
}] }]

219
src/views/Task/List.vue Normal file
View File

@ -0,0 +1,219 @@
<template>
<div class="task-list-container">
<el-card>
<template #header>
<div class="card-header">
<span>任务列表</span>
<el-button type="primary" size="small" @click="refreshTasks">刷新</el-button>
</div>
</template>
<div v-if="loading" class="loading-content">
<el-skeleton :rows="5" animated />
</div>
<div v-else-if="taskList.length === 0" class="empty-content">
暂无运行中的任务
</div>
<el-table v-else :data="taskList" border style="width: 100%">
<el-table-column prop="id" label="任务ID" width="180" />
<el-table-column prop="fileName" label="文件名称" />
<el-table-column prop="shopNames" label="店铺名称" />
<el-table-column prop="dataNum" label="数据总数" width="100" />
<el-table-column prop="taskStatus" label="状态" width="100">
<template #default="scope">
<el-tag :type="getTaskStatusType(scope.row.taskStatus)">
{{ getTaskStatusText(scope.row.taskStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="handleStopTask(scope.row.id)"
:loading="stoppingTasks.includes(scope.row.id)"
:disabled="scope.row.taskStatus !== '1'"
>
停止任务
</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="detailVisible" title="任务详情" width="500px">
<el-descriptions :column="1" border>
<el-descriptions-item label="任务ID">{{ currentTask.id }}</el-descriptions-item>
<el-descriptions-item label="文件名称">{{ currentTask.fileName }}</el-descriptions-item>
<el-descriptions-item label="店铺名称">{{ currentTask.shopNames }}</el-descriptions-item>
<el-descriptions-item label="数据总数">{{ currentTask.dataNum }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ currentTask.createTime }}</el-descriptions-item>
<el-descriptions-item label="执行进度">
<el-progress
:percentage="calculateProgress(currentTask)"
:status="currentTask.taskStatus === '1' ? 'success' : 'exception'"
/>
</el-descriptions-item>
<el-descriptions-item label="执行消息">
<div class="task-msg">{{ currentTask.msg }}</div>
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { taskApi } from '@/api/modules/task'
//
const taskList = ref([])
const loading = ref(false)
const stoppingTasks = ref([])
const detailVisible = ref(false)
const currentTask = ref({})
//
const fetchTaskList = async () => {
loading.value = true
try {
const res = await taskApi.getRunningTasks()
console.log(res)
if (res.code === 200) {
taskList.value = res.data || []
} else {
ElMessage.error(res.data.message || '获取任务列表失败')
}
} catch (error) {
console.error('获取任务列表出错:', error)
ElMessage.error('获取任务列表失败: ' + (error.message || '未知错误'))
} finally {
loading.value = false
}
}
//
const refreshTasks = () => {
fetchTaskList()
}
//
const handleStopTask = async (taskId) => {
console.log("taskId",taskId)
try {
await ElMessageBox.confirm(
`确定要停止任务吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
stoppingTasks.value.push(taskId)
const res = await taskApi.stopTask(taskId)
if (res.code === 200) {
ElMessage.success('任务已成功停止')
//
fetchTaskList()
} else {
ElMessage.error(res.data.message || '停止任务失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('停止任务出错:', error)
ElMessage.error('停止任务失败: ' + (error.message || '未知错误'))
}
} finally {
stoppingTasks.value = stoppingTasks.value.filter(id => id !== taskId)
}
}
//
const getTaskStatusText = (status) => {
const statusMap = {
'0': '未开始',
'1': '执行中',
'2': '已完成',
'3': '已中止',
'4': '执行失败'
}
return statusMap[status] || '未知状态'
}
const getTaskStatusType = (status) => {
const typeMap = {
'0': 'info',
'1': 'success',
'2': 'success',
'3': 'warning',
'4': 'danger'
}
return typeMap[status] || 'info'
}
//
const showTaskDetail = (task) => {
currentTask.value = task
detailVisible.value = true
}
//
const calculateProgress = (task) => {
if (!task || !task.msg) return 0
try {
//
const totalMatch = task.msg.match(/总执行数据:(\d+)条/)
const executedMatch = task.msg.match(/已执行条数:(\d+)条/)
if (totalMatch && executedMatch) {
const total = parseInt(totalMatch[1])
const executed = parseInt(executedMatch[1])
if (total > 0) {
return Math.floor((executed / total) * 100)
}
}
return 0
} catch (e) {
return 0
}
}
//
onMounted(() => {
fetchTaskList()
})
</script>
<style scoped>
.task-list-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.empty-content, .loading-content {
min-height: 300px;
display: flex;
justify-content: center;
align-items: center;
color: #909399;
}
.task-msg {
white-space: pre-line;
max-height: 150px;
overflow-y: auto;
padding: 8px;
background-color: #f8f8f8;
border-radius: 4px;
}
</style>

View File

@ -119,8 +119,13 @@ export default {
const res = await getRoleList() const res = await getRoleList()
roleList.value = res.data || [] roleList.value = res.data || []
} catch (error) { } catch (error) {
console.error('获取角色列表失败', error) console.log(error)
ElMessage.error('获取角色列表失败') if(error.status === 403){
ElMessage.error("您没有权限执行此操作")
}else{
console.error('获取角色列表失败', error)
ElMessage.error('获取角色列表失败')
}
} finally { } finally {
loading.value = false loading.value = false
} }