fix: 修复定时任务模块问题
All checks were successful
continuous-integration/drone/push Build is passing

1. 修复 SDK 文档 API 路由顺序问题
   - 将静态路由 /sdk-docs, /test-script, /secrets 移到动态路由 /{task_id} 之前
   - 解决 "请求参数验证失败" 错误

2. 优化错误页面体验
   - 使用 sessionStorage 传递错误信息,URL 保持干净
   - 使用 router.replace 替代 push,浏览器返回不会停留在错误页
   - 记录来源页面,支持正确返回

3. 增强网络错误处理
   - 区分超时、网络错误、服务不可用
   - 后端未启动时显示友好的 "服务暂时不可用" 提示

4. 添加定时任务模块文档
This commit is contained in:
2026-01-28 18:18:04 +08:00
parent 262a1b409f
commit b0f7d1ba9e
4 changed files with 649 additions and 223 deletions

View File

@@ -18,9 +18,21 @@ function parseApiError(error) {
status: 500
}
// 网络错误(后端未启动、网络断开等)
if (!error.response) {
result.code = 'NETWORK_ERROR'
result.message = '网络连接失败,请检查网络后重试'
if (error.code === 'ECONNABORTED') {
result.code = 'TIMEOUT_ERROR'
result.message = '请求超时,请稍后重试'
result.status = 0
} else if (error.message?.includes('Network Error')) {
result.code = 'SERVICE_UNAVAILABLE'
result.message = '服务暂时不可用,请稍后重试'
result.status = 503
} else {
result.code = 'NETWORK_ERROR'
result.message = '网络连接失败,请检查网络后重试'
result.status = 0
}
return result
}
@@ -40,18 +52,23 @@ function parseApiError(error) {
}
/**
* 跳转到错误页面
* 跳转到错误页面(使用 sessionStorage + replace不影响浏览器历史
*/
function navigateToErrorPage(errorInfo) {
router.push({
name: 'Error',
query: {
code: errorInfo.code,
message: errorInfo.message,
trace_id: errorInfo.traceId,
status: String(errorInfo.status)
}
})
// 记录当前页面路径(用于返回)
sessionStorage.setItem('errorFromPath', router.currentRoute.value.fullPath)
// 保存错误信息到 sessionStorage不会显示在 URL 中)
sessionStorage.setItem('errorInfo', JSON.stringify({
code: errorInfo.code,
message: errorInfo.message,
traceId: errorInfo.traceId,
status: errorInfo.status,
timestamp: Date.now()
}))
// 使用 replace 而不是 push这样浏览器返回时不会停留在错误页
router.replace({ name: 'Error' })
}
// 请求拦截器
@@ -75,6 +92,15 @@ api.interceptors.response.use(
console.error(`[API Error] ${errorInfo.code}: ${errorInfo.message}${traceLog}`)
// 严重错误列表(跳转错误页)
const criticalErrors = [
'INTERNAL_ERROR',
'SERVICE_UNAVAILABLE',
'GATEWAY_ERROR',
'NETWORK_ERROR',
'TIMEOUT_ERROR'
]
if (error.response?.status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('user')
@@ -82,8 +108,8 @@ api.interceptors.response.use(
ElMessage.error('登录已过期,请重新登录')
} else if (error.response?.status === 403) {
ElMessage.error('没有权限执行此操作')
} else if (['INTERNAL_ERROR', 'SERVICE_UNAVAILABLE', 'GATEWAY_ERROR'].includes(errorInfo.code)) {
// 严重错误跳转到错误页面
} else if (criticalErrors.includes(errorInfo.code)) {
// 严重错误(包括网络错误、服务不可用)跳转到错误页面
navigateToErrorPage(errorInfo)
} else {
// 普通错误显示消息

View File

@@ -1,33 +1,69 @@
<script setup>
/**
* 统一错误页面
* - 从 sessionStorage 读取错误信息,不污染 URL
* - 使用 replace 跳转,支持浏览器返回
*/
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElButton, ElMessage } from 'element-plus'
const route = useRoute()
const router = useRouter()
const errorCode = computed(() => route.query.code || 'UNKNOWN_ERROR')
const errorMessage = computed(() => route.query.message || '发生了未知错误')
const traceId = computed(() => route.query.trace_id || '')
const statusCode = computed(() => route.query.status || '500')
// 错误信息
const errorCode = ref('UNKNOWN_ERROR')
const errorMessage = ref('发生了未知错误')
const traceId = ref('')
const statusCode = ref('500')
const copied = ref(false)
const errorConfig = computed(() => {
const configs = {
'UNAUTHORIZED': { icon: 'Lock', title: '访问受限', color: '#e67e22' },
'FORBIDDEN': { icon: 'CircleClose', title: '权限不足', color: '#e74c3c' },
'NOT_FOUND': { icon: 'Search', title: '页面未找到', color: '#3498db' },
'VALIDATION_ERROR': { icon: 'Warning', title: '请求参数错误', color: '#f39c12' },
'INTERNAL_ERROR': { icon: 'Close', title: '服务器错误', color: '#e74c3c' },
'SERVICE_UNAVAILABLE': { icon: 'Tools', title: '服务暂时不可用', color: '#9b59b6' },
'NETWORK_ERROR': { icon: 'Connection', title: '网络连接失败', color: '#95a5a6' },
'UNKNOWN_ERROR': { icon: 'QuestionFilled', title: '未知错误', color: '#7f8c8d' }
// 记录来源页面(用于返回)
const fromPath = ref('')
// 错误类型配置
const errorConfigs = {
'UNAUTHORIZED': { icon: 'Lock', title: '访问受限', color: '#e67e22' },
'FORBIDDEN': { icon: 'CircleClose', title: '权限不足', color: '#e74c3c' },
'NOT_FOUND': { icon: 'Search', title: '页面未找到', color: '#3498db' },
'VALIDATION_ERROR': { icon: 'Warning', title: '请求参数错误', color: '#f39c12' },
'INTERNAL_ERROR': { icon: 'Close', title: '服务器错误', color: '#e74c3c' },
'SERVICE_UNAVAILABLE': { icon: 'Tools', title: '服务暂时不可用', color: '#9b59b6' },
'NETWORK_ERROR': { icon: 'Connection', title: '网络连接失败', color: '#95a5a6' },
'TIMEOUT_ERROR': { icon: 'Timer', title: '请求超时', color: '#95a5a6' },
'UNKNOWN_ERROR': { icon: 'QuestionFilled', title: '未知错误', color: '#7f8c8d' }
}
const errorConfig = ref(errorConfigs['UNKNOWN_ERROR'])
onMounted(() => {
// 从 sessionStorage 读取错误信息
const stored = sessionStorage.getItem('errorInfo')
if (stored) {
try {
const info = JSON.parse(stored)
// 检查时效性5分钟内有效
if (Date.now() - info.timestamp < 5 * 60 * 1000) {
errorCode.value = info.code || 'UNKNOWN_ERROR'
errorMessage.value = info.message || '发生了未知错误'
traceId.value = info.traceId || ''
statusCode.value = String(info.status || 500)
errorConfig.value = errorConfigs[errorCode.value] || errorConfigs['UNKNOWN_ERROR']
}
// 读取后清除(避免刷新时重复显示旧错误)
sessionStorage.removeItem('errorInfo')
} catch (e) {
console.error('Failed to parse error info', e)
}
}
return configs[errorCode.value] || configs['UNKNOWN_ERROR']
// 记录来源页面
fromPath.value = sessionStorage.getItem('errorFromPath') || '/dashboard'
sessionStorage.removeItem('errorFromPath')
})
onUnmounted(() => {
// 确保清理
sessionStorage.removeItem('errorInfo')
})
const copyTraceId = async () => {
@@ -44,7 +80,15 @@ const copyTraceId = async () => {
}
const goHome = () => router.push('/dashboard')
const retry = () => router.back()
// 返回之前的页面
const goBack = () => {
if (fromPath.value && fromPath.value !== '/error') {
router.push(fromPath.value)
} else {
router.push('/dashboard')
}
}
</script>
<template>
@@ -72,7 +116,7 @@ const retry = () => router.back()
</div>
<div class="action-buttons">
<el-button type="primary" @click="retry">重试</el-button>
<el-button type="primary" @click="goBack">返回</el-button>
<el-button @click="goHome">返回首页</el-button>
</div>
</div>