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

@@ -75,6 +75,191 @@ class TestScriptRequest(BaseModel):
params: Optional[dict] = None
# ==================== Static Routes (must be before dynamic routes) ====================
@router.get("/sdk-docs")
async def get_sdk_docs():
"""获取SDK文档"""
return {
"functions": [
{
"name": "log",
"signature": "log(message: str, level: str = 'INFO')",
"description": "记录日志",
"example": "log('处理完成', 'INFO')"
},
{
"name": "print",
"signature": "print(*args)",
"description": "打印输出",
"example": "print('Hello', 'World')"
},
{
"name": "ai",
"signature": "ai(prompt: str, system: str = None, model: str = None, temperature: float = 0.7)",
"description": "调用AI模型",
"example": "result = ai('生成一段问候语', system='你是友善的助手')"
},
{
"name": "dingtalk",
"signature": "dingtalk(webhook: str, content: str, title: str = None, at_all: bool = False)",
"description": "发送钉钉消息",
"example": "dingtalk(webhook_url, '# 标题\\n内容')"
},
{
"name": "wecom",
"signature": "wecom(webhook: str, content: str, msg_type: str = 'markdown')",
"description": "发送企微消息",
"example": "wecom(webhook_url, '消息内容')"
},
{
"name": "http_get",
"signature": "http_get(url: str, headers: dict = None, params: dict = None)",
"description": "发起GET请求",
"example": "resp = http_get('https://api.example.com/data')"
},
{
"name": "http_post",
"signature": "http_post(url: str, data: any = None, headers: dict = None)",
"description": "发起POST请求",
"example": "resp = http_post('https://api.example.com/submit', {'key': 'value'})"
},
{
"name": "db_query",
"signature": "db_query(sql: str, params: dict = None)",
"description": "执行只读SQL查询",
"example": "rows = db_query('SELECT * FROM users WHERE status = :status', {'status': 1})"
},
{
"name": "get_var",
"signature": "get_var(key: str, default: any = None)",
"description": "获取持久化变量",
"example": "counter = get_var('counter', 0)"
},
{
"name": "set_var",
"signature": "set_var(key: str, value: any)",
"description": "设置持久化变量",
"example": "set_var('counter', counter + 1)"
},
{
"name": "del_var",
"signature": "del_var(key: str)",
"description": "删除持久化变量",
"example": "del_var('temp_data')"
},
{
"name": "get_param",
"signature": "get_param(key: str, default: any = None)",
"description": "获取任务参数",
"example": "prompt = get_param('prompt', '默认提示词')"
},
{
"name": "get_params",
"signature": "get_params()",
"description": "获取所有任务参数",
"example": "params = get_params()"
},
{
"name": "get_tenants",
"signature": "get_tenants(app_code: str = None)",
"description": "获取租户列表",
"example": "tenants = get_tenants('notification-service')"
},
{
"name": "get_tenant_config",
"signature": "get_tenant_config(tenant_id: str, app_code: str, key: str = None)",
"description": "获取租户的应用配置",
"example": "webhook = get_tenant_config('tenant1', 'notification-service', 'dingtalk_webhook')"
},
{
"name": "get_all_tenant_configs",
"signature": "get_all_tenant_configs(app_code: str)",
"description": "获取所有租户的应用配置",
"example": "configs = get_all_tenant_configs('notification-service')"
},
{
"name": "get_secret",
"signature": "get_secret(key: str)",
"description": "获取密钥(优先租户级)",
"example": "api_key = get_secret('api_key')"
}
],
"variables": [
{"name": "task_id", "description": "当前任务ID"},
{"name": "tenant_id", "description": "当前租户ID"},
{"name": "trace_id", "description": "当前执行追踪ID"}
],
"libraries": [
{"name": "json", "description": "JSON处理"},
{"name": "re", "description": "正则表达式"},
{"name": "math", "description": "数学函数"},
{"name": "random", "description": "随机数"},
{"name": "hashlib", "description": "哈希函数"},
{"name": "base64", "description": "Base64编解码"},
{"name": "datetime", "description": "日期时间处理"},
{"name": "timedelta", "description": "时间差"},
{"name": "urlencode/quote/unquote", "description": "URL编码"}
]
}
@router.post("/test-script")
async def test_script(data: TestScriptRequest, db: Session = Depends(get_db)):
"""测试脚本执行"""
executor = ScriptExecutor(db)
result = executor.test_script(
script_content=data.script_content,
task_id=0,
tenant_id=data.tenant_id,
params=data.params
)
return result
@router.get("/secrets")
async def list_secrets(
tenant_id: Optional[str] = None,
db: Session = Depends(get_db)
):
"""获取密钥列表"""
query = db.query(Secret)
if tenant_id:
query = query.filter(Secret.tenant_id == tenant_id)
items = query.order_by(desc(Secret.created_at)).all()
return {
"items": [
{
"id": s.id,
"tenant_id": s.tenant_id,
"secret_key": s.secret_key,
"description": s.description,
"created_at": s.created_at,
"updated_at": s.updated_at
}
for s in items
]
}
@router.post("/secrets")
async def create_secret(data: SecretCreate, db: Session = Depends(get_db)):
"""创建密钥"""
secret = Secret(
tenant_id=data.tenant_id,
secret_key=data.secret_key,
secret_value=data.secret_value,
description=data.description
)
db.add(secret)
db.commit()
db.refresh(secret)
return {"success": True, "id": secret.id}
# ==================== Task CRUD ====================
@router.get("")
@@ -272,194 +457,7 @@ async def get_task_logs(
}
# ==================== Script Testing ====================
@router.post("/test-script")
async def test_script(data: TestScriptRequest, db: Session = Depends(get_db)):
"""测试脚本执行"""
executor = ScriptExecutor(db)
result = executor.test_script(
script_content=data.script_content,
task_id=0,
tenant_id=data.tenant_id,
params=data.params
)
return result
# ==================== SDK Documentation ====================
@router.get("/sdk-docs")
async def get_sdk_docs():
"""获取SDK文档"""
return {
"functions": [
{
"name": "log",
"signature": "log(message: str, level: str = 'INFO')",
"description": "记录日志",
"example": "log('处理完成', 'INFO')"
},
{
"name": "print",
"signature": "print(*args)",
"description": "打印输出",
"example": "print('Hello', 'World')"
},
{
"name": "ai",
"signature": "ai(prompt: str, system: str = None, model: str = None, temperature: float = 0.7)",
"description": "调用AI模型",
"example": "result = ai('生成一段问候语', system='你是友善的助手')"
},
{
"name": "dingtalk",
"signature": "dingtalk(webhook: str, content: str, title: str = None, at_all: bool = False)",
"description": "发送钉钉消息",
"example": "dingtalk(webhook_url, '# 标题\\n内容')"
},
{
"name": "wecom",
"signature": "wecom(webhook: str, content: str, msg_type: str = 'markdown')",
"description": "发送企微消息",
"example": "wecom(webhook_url, '消息内容')"
},
{
"name": "http_get",
"signature": "http_get(url: str, headers: dict = None, params: dict = None)",
"description": "发起GET请求",
"example": "resp = http_get('https://api.example.com/data')"
},
{
"name": "http_post",
"signature": "http_post(url: str, data: any = None, headers: dict = None)",
"description": "发起POST请求",
"example": "resp = http_post('https://api.example.com/submit', {'key': 'value'})"
},
{
"name": "db_query",
"signature": "db_query(sql: str, params: dict = None)",
"description": "执行只读SQL查询",
"example": "rows = db_query('SELECT * FROM users WHERE status = :status', {'status': 1})"
},
{
"name": "get_var",
"signature": "get_var(key: str, default: any = None)",
"description": "获取持久化变量",
"example": "counter = get_var('counter', 0)"
},
{
"name": "set_var",
"signature": "set_var(key: str, value: any)",
"description": "设置持久化变量",
"example": "set_var('counter', counter + 1)"
},
{
"name": "del_var",
"signature": "del_var(key: str)",
"description": "删除持久化变量",
"example": "del_var('temp_data')"
},
{
"name": "get_param",
"signature": "get_param(key: str, default: any = None)",
"description": "获取任务参数",
"example": "prompt = get_param('prompt', '默认提示词')"
},
{
"name": "get_params",
"signature": "get_params()",
"description": "获取所有任务参数",
"example": "params = get_params()"
},
{
"name": "get_tenants",
"signature": "get_tenants(app_code: str = None)",
"description": "获取租户列表",
"example": "tenants = get_tenants('notification-service')"
},
{
"name": "get_tenant_config",
"signature": "get_tenant_config(tenant_id: str, app_code: str, key: str = None)",
"description": "获取租户的应用配置",
"example": "webhook = get_tenant_config('tenant1', 'notification-service', 'dingtalk_webhook')"
},
{
"name": "get_all_tenant_configs",
"signature": "get_all_tenant_configs(app_code: str)",
"description": "获取所有租户的应用配置",
"example": "configs = get_all_tenant_configs('notification-service')"
},
{
"name": "get_secret",
"signature": "get_secret(key: str)",
"description": "获取密钥(优先租户级)",
"example": "api_key = get_secret('api_key')"
}
],
"variables": [
{"name": "task_id", "description": "当前任务ID"},
{"name": "tenant_id", "description": "当前租户ID"},
{"name": "trace_id", "description": "当前执行追踪ID"}
],
"libraries": [
{"name": "json", "description": "JSON处理"},
{"name": "re", "description": "正则表达式"},
{"name": "math", "description": "数学函数"},
{"name": "random", "description": "随机数"},
{"name": "hashlib", "description": "哈希函数"},
{"name": "base64", "description": "Base64编解码"},
{"name": "datetime", "description": "日期时间处理"},
{"name": "timedelta", "description": "时间差"},
{"name": "urlencode/quote/unquote", "description": "URL编码"}
]
}
# ==================== Secrets ====================
@router.get("/secrets")
async def list_secrets(
tenant_id: Optional[str] = None,
db: Session = Depends(get_db)
):
"""获取密钥列表"""
query = db.query(Secret)
if tenant_id:
query = query.filter(Secret.tenant_id == tenant_id)
items = query.order_by(desc(Secret.created_at)).all()
return {
"items": [
{
"id": s.id,
"tenant_id": s.tenant_id,
"secret_key": s.secret_key,
"description": s.description,
"created_at": s.created_at,
"updated_at": s.updated_at
}
for s in items
]
}
@router.post("/secrets")
async def create_secret(data: SecretCreate, db: Session = Depends(get_db)):
"""创建密钥"""
secret = Secret(
tenant_id=data.tenant_id,
secret_key=data.secret_key,
secret_value=data.secret_value,
description=data.description
)
db.add(secret)
db.commit()
db.refresh(secret)
return {"success": True, "id": secret.id}
# ==================== Secrets (dynamic routes) ====================
@router.put("/secrets/{secret_id}")
async def update_secret(secret_id: int, data: SecretUpdate, db: Session = Depends(get_db)):

358
docs/scheduled-tasks.md Normal file
View File

@@ -0,0 +1,358 @@
# 定时任务系统文档
## 功能概述
平台定时任务系统,支持 Python 脚本或 Webhook 定时执行,执行结果可自动推送到钉钉/企微机器人。
**核心能力**
- 脚本执行:安全沙箱运行 Python 脚本,内置 AI、HTTP、数据库等 SDK
- 调度方式:指定时间点(多选)或 CRON 表达式
- 消息推送:支持钉钉/企微机器人所有消息格式markdown、actionCard、feedCard 等)
- 失败处理:支持重试和告警通知
---
## 数据库表
### platform_scheduled_tasks定时任务表
```sql
CREATE TABLE platform_scheduled_tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id VARCHAR(50) COMMENT '租户ID空为全局任务',
task_name VARCHAR(100) NOT NULL COMMENT '任务名称',
task_desc VARCHAR(500) COMMENT '任务描述',
schedule_type ENUM('simple', 'cron') NOT NULL DEFAULT 'simple',
time_points JSON COMMENT '时间点列表 ["08:00", "12:00"]',
cron_expression VARCHAR(100) COMMENT 'CRON表达式',
timezone VARCHAR(50) DEFAULT 'Asia/Shanghai',
execution_type ENUM('webhook', 'script') NOT NULL DEFAULT 'script',
webhook_url VARCHAR(500),
script_content TEXT COMMENT 'Python脚本内容',
script_deps TEXT COMMENT '脚本依赖',
input_params JSON COMMENT '输入参数',
retry_count INT DEFAULT 0,
retry_interval INT DEFAULT 60,
alert_on_failure TINYINT(1) DEFAULT 0,
alert_webhook VARCHAR(500),
notify_channels JSON COMMENT '通知渠道ID列表',
notify_wecom_app_id INT COMMENT '企微应用ID',
is_enabled TINYINT(1) DEFAULT 1,
last_run_at DATETIME,
last_run_status ENUM('success', 'failed', 'running'),
last_run_message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
### platform_task_notify_channels通知渠道表
```sql
CREATE TABLE platform_task_notify_channels (
id INT AUTO_INCREMENT PRIMARY KEY,
tenant_id VARCHAR(50) NOT NULL COMMENT '租户ID',
channel_name VARCHAR(100) NOT NULL COMMENT '渠道名称',
channel_type ENUM('dingtalk_bot', 'wecom_bot') NOT NULL,
webhook_url VARCHAR(500) NOT NULL,
sign_secret VARCHAR(200) COMMENT '钉钉加签密钥',
description VARCHAR(255),
is_enabled TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
### platform_task_logs执行日志表
```sql
CREATE TABLE platform_task_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_id INT NOT NULL,
tenant_id VARCHAR(50),
trace_id VARCHAR(100),
status ENUM('running', 'success', 'failed'),
started_at DATETIME,
finished_at DATETIME,
duration_ms INT,
output TEXT,
error TEXT,
retry_count INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
---
## 后端文件结构
```
backend/app/
├── models/
│ ├── scheduled_task.py # ScheduledTask, TaskLog, ScriptVar, Secret 模型
│ └── notification_channel.py # TaskNotifyChannel 模型
├── routers/
│ ├── tasks.py # 定时任务 API (/api/scheduled-tasks)
│ └── notification_channels.py # 通知渠道 API (/api/notification-channels)
└── services/
├── scheduler.py # APScheduler 调度服务
├── script_executor.py # 脚本执行器(安全沙箱)
└── script_sdk.py # 脚本内置 SDK
```
---
## 脚本 SDK 文档
### 内置函数
```python
# 日志
log(message) # 记录日志
print(message) # 打印输出
# AI 调用
ai(prompt, system=None, model='deepseek-chat') # 调用 AI
# 通知发送(直接发送,不走 result
dingtalk(webhook_url, content, title='通知')
wecom(webhook_url, content)
# HTTP 请求
http_get(url, headers=None, params=None)
http_post(url, data=None, json=None, headers=None)
# 数据库查询(只读)
db_query(sql, params=None)
# 变量存储(跨执行持久化)
get_var(key, default=None)
set_var(key, value)
del_var(key)
# 任务参数
get_param(key, default=None) # 获取单个参数
get_params() # 获取所有参数
# 租户相关
get_tenants() # 获取所有租户
get_tenant_config(tenant_id, app_code, key) # 获取租户配置
get_all_tenant_configs(app_code, key) # 获取所有租户的配置
# 密钥
get_secret(key) # 获取密钥
```
### 内置变量
```python
task_id # 当前任务ID
tenant_id # 当前租户ID可能为空
trace_id # 追踪ID
```
### 内置模块(无需 import
```python
datetime # datetime.now(), datetime.strptime()
date # date.today()
timedelta # timedelta(days=1)
time # time.sleep(), time.time()
json # json.dumps(), json.loads()
re # re.search(), re.match()
math # math.ceil(), math.floor()
random # random.randint(), random.choice()
hashlib # hashlib.md5()
base64 # base64.b64encode()
```
---
## 消息格式result 变量)
### 基础格式(默认 markdown
```python
result = {
'content': 'Markdown 内容',
'title': '消息标题'
}
```
### 钉钉 ActionCard交互卡片
```python
result = {
'msg_type': 'actionCard',
'title': '卡片标题',
'content': '''### 正文内容
| 列1 | 列2 |
|:---:|:---:|
| A | B |
''',
'btn_orientation': '1', # 0-竖向 1-横向
'buttons': [
{'title': '按钮1', 'url': 'https://...'},
{'title': '按钮2', 'url': 'https://...'}
]
}
```
### 钉钉 FeedCard信息流
```python
result = {
'msg_type': 'feedCard',
'links': [
{'title': '标题1', 'url': 'https://...', 'pic_url': 'https://...'},
{'title': '标题2', 'url': 'https://...', 'pic_url': 'https://...'}
]
}
```
### 钉钉 Link链接消息
```python
result = {
'msg_type': 'link',
'title': '链接标题',
'content': '链接描述',
'url': 'https://...',
'pic_url': 'https://...'
}
```
### 企微 News图文消息
```python
result = {
'msg_type': 'news',
'articles': [
{
'title': '文章标题',
'description': '文章描述',
'url': 'https://...',
'picurl': 'https://...'
}
]
}
```
### 企微 Template Card模板卡片
```python
result = {
'msg_type': 'template_card',
'card_type': 'text_notice', # text_notice / news_notice / button_interaction
'title': '卡片标题',
'content': '卡片内容',
'horizontal_list': [
{'keyname': '申请人', 'value': '张三'},
{'keyname': '金额', 'value': '¥5,000'}
],
'jump_list': [
{'type': 1, 'title': '查看详情', 'url': 'https://...'}
]
}
```
---
## API 端点
### 定时任务
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/scheduled-tasks | 任务列表 |
| GET | /api/scheduled-tasks/{id} | 任务详情 |
| POST | /api/scheduled-tasks | 创建任务 |
| PUT | /api/scheduled-tasks/{id} | 更新任务 |
| DELETE | /api/scheduled-tasks/{id} | 删除任务 |
| POST | /api/scheduled-tasks/{id}/toggle | 启用/禁用 |
| POST | /api/scheduled-tasks/{id}/run | 立即执行 |
| GET | /api/scheduled-tasks/{id}/logs | 执行日志 |
| POST | /api/scheduled-tasks/test-script | 测试脚本 |
| GET | /api/scheduled-tasks/sdk-docs | SDK 文档 |
### 通知渠道
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/notification-channels | 渠道列表 |
| POST | /api/notification-channels | 创建渠道 |
| PUT | /api/notification-channels/{id} | 更新渠道 |
| DELETE | /api/notification-channels/{id} | 删除渠道 |
| POST | /api/notification-channels/{id}/test | 测试渠道 |
---
## 前端文件
```
frontend/src/views/
├── scheduled-tasks/
│ └── index.vue # 定时任务管理页面
└── notification-channels/
└── index.vue # 通知渠道管理页面
```
---
## 示例脚本
### 基础示例
```python
# 无需 import模块已内置
log('任务开始执行')
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
prompt = get_param('prompt', '默认提示词')
content = ai(prompt, system='你是一个助手')
result = {
'title': '每日推送',
'content': f'**生成时间**: {now}\n\n{content}'
}
log('任务执行完成')
```
### 复杂卡片示例
```python
log('生成销售日报')
now = datetime.now()
today = now.strftime('%Y年%m月%d')
# 模拟数据
revenue = random.randint(50000, 150000)
result = {
'msg_type': 'actionCard',
'title': f'销售日报 | {today}',
'content': f'''### 今日业绩
| 指标 | 数值 |
|:---:|:---:|
| 销售额 | **¥{revenue:,}** |
| 订单数 | **{random.randint(40, 80)}** |
> 点击查看详情
''',
'buttons': [
{'title': '查看详情', 'url': 'https://example.com/report'}
]
}
```
---
## 部署信息
- **测试环境**: https://platform.test.ai.ireborn.com.cn
- **数据库**: new_qiqi (测试) / new_platform_prod (生产)
- **Docker 容器**: platform-backend-test / platform-frontend-test

View File

@@ -18,9 +18,21 @@ function parseApiError(error) {
status: 500
}
// 网络错误(后端未启动、网络断开等)
if (!error.response) {
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: {
// 记录当前页面路径(用于返回)
sessionStorage.setItem('errorFromPath', router.currentRoute.value.fullPath)
// 保存错误信息到 sessionStorage不会显示在 URL 中)
sessionStorage.setItem('errorInfo', JSON.stringify({
code: errorInfo.code,
message: errorInfo.message,
trace_id: errorInfo.traceId,
status: String(errorInfo.status)
}
})
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,23 +1,27 @@
<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 = {
// 记录来源页面(用于返回)
const fromPath = ref('')
// 错误类型配置
const errorConfigs = {
'UNAUTHORIZED': { icon: 'Lock', title: '访问受限', color: '#e67e22' },
'FORBIDDEN': { icon: 'CircleClose', title: '权限不足', color: '#e74c3c' },
'NOT_FOUND': { icon: 'Search', title: '页面未找到', color: '#3498db' },
@@ -25,9 +29,41 @@ const errorConfig = computed(() => {
'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']
}
return configs[errorCode.value] || configs['UNKNOWN_ERROR']
// 读取后清除(避免刷新时重复显示旧错误)
sessionStorage.removeItem('errorInfo')
} catch (e) {
console.error('Failed to parse error info', e)
}
}
// 记录来源页面
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>