bug修复
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
管理后台部署脚本 - 将构建好的管理后台文件上传到服务器
|
||||
使用方法: python deploy.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# 配置变量
|
||||
SERVER_IP = "101.200.208.45"
|
||||
USERNAME = "root"
|
||||
REMOTE_PATH = "/data/www/emotion-museum-admin"
|
||||
|
||||
# 本地路径
|
||||
SCRIPT_DIR = Path(__file__).parent.absolute()
|
||||
DIST_DIR = SCRIPT_DIR / "dist"
|
||||
|
||||
|
||||
class Colors:
|
||||
"""终端颜色"""
|
||||
GREEN = '\033[32m'
|
||||
RED = '\033[31m'
|
||||
YELLOW = '\033[33m'
|
||||
RESET = '\033[0m'
|
||||
|
||||
|
||||
def log_info(msg):
|
||||
"""打印信息日志"""
|
||||
print(f"{Colors.GREEN}✅{Colors.RESET} {msg}")
|
||||
|
||||
|
||||
def log_error(msg):
|
||||
"""打印错误日志"""
|
||||
print(f"{Colors.RED}❌{Colors.RESET} {msg}")
|
||||
|
||||
|
||||
def log_step(msg):
|
||||
"""打印步骤日志"""
|
||||
print(f"📦 {msg}")
|
||||
|
||||
|
||||
def run_command(cmd, cwd=None, shell=True, capture=True):
|
||||
"""执行本地命令"""
|
||||
try:
|
||||
if capture:
|
||||
result = subprocess.run(cmd, cwd=cwd, shell=shell, capture_output=True, text=True)
|
||||
return result.returncode == 0, result.stdout, result.stderr
|
||||
else:
|
||||
result = subprocess.run(cmd, cwd=cwd, shell=shell)
|
||||
return result.returncode == 0, "", ""
|
||||
except Exception as e:
|
||||
return False, "", str(e)
|
||||
|
||||
|
||||
def check_npm():
|
||||
"""检查npm是否安装"""
|
||||
success, _, _ = run_command("npm --version")
|
||||
if not success:
|
||||
log_error("错误: 未找到npm命令,请先安装Node.js")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_scp():
|
||||
"""检查scp是否可用"""
|
||||
success, _, _ = run_command("scp -V 2>&1 || echo ok")
|
||||
# scp通常没有--version,但命令存在即可
|
||||
return True
|
||||
|
||||
|
||||
def clean_dist():
|
||||
"""清理旧的构建文件"""
|
||||
log_step("🧹 清理旧的构建文件...")
|
||||
if DIST_DIR.exists():
|
||||
shutil.rmtree(DIST_DIR)
|
||||
|
||||
|
||||
def build_project():
|
||||
"""构建项目"""
|
||||
log_step("开始构建管理后台项目(生产环境)...")
|
||||
|
||||
os.chdir(SCRIPT_DIR)
|
||||
|
||||
# 设置环境变量并执行构建
|
||||
env = os.environ.copy()
|
||||
env["NODE_ENV"] = "production"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
"npm run build",
|
||||
shell=True,
|
||||
cwd=SCRIPT_DIR,
|
||||
env=env
|
||||
)
|
||||
if result.returncode != 0:
|
||||
log_error("管理后台项目构建失败,请检查代码")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
log_error(f"构建失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
log_info("管理后台项目构建成功")
|
||||
|
||||
|
||||
def verify_dist():
|
||||
"""验证dist目录是否存在"""
|
||||
if not DIST_DIR.exists():
|
||||
log_error("错误: 构建后dist目录仍不存在,请检查构建配置")
|
||||
sys.exit(1)
|
||||
|
||||
# 检查关键文件
|
||||
index_file = DIST_DIR / "index.html"
|
||||
assets_dir = DIST_DIR / "assets"
|
||||
|
||||
if not index_file.exists():
|
||||
log_error("错误: dist/index.html 不存在")
|
||||
sys.exit(1)
|
||||
|
||||
if not assets_dir.exists():
|
||||
log_error("错误: dist/assets 目录不存在")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create_remote_dir():
|
||||
"""创建远程目录"""
|
||||
log_step("📁 创建远程目录...")
|
||||
cmd = f'ssh {USERNAME}@{SERVER_IP} "mkdir -p {REMOTE_PATH}"'
|
||||
success, _, stderr = run_command(cmd)
|
||||
if not success:
|
||||
log_error(f"创建远程目录失败: {stderr}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def upload_files():
|
||||
"""上传文件到服务器"""
|
||||
log_step("📤 上传文件到服务器...")
|
||||
|
||||
print(f"正在上传文件到服务器 {SERVER_IP}...")
|
||||
|
||||
# 上传 index.html
|
||||
index_file = DIST_DIR / "index.html"
|
||||
cmd1 = f'scp "{index_file}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/'
|
||||
success1, _, stderr1 = run_command(cmd1)
|
||||
|
||||
if not success1:
|
||||
log_error(f"上传 index.html 失败: {stderr1}")
|
||||
return False
|
||||
|
||||
# 上传 assets 目录
|
||||
assets_dir = DIST_DIR / "assets"
|
||||
cmd2 = f'scp -r "{assets_dir}" {USERNAME}@{SERVER_IP}:{REMOTE_PATH}/'
|
||||
success2, _, stderr2 = run_command(cmd2)
|
||||
|
||||
if not success2:
|
||||
log_error(f"上传 assets 目录失败: {stderr2}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def set_permissions():
|
||||
"""设置文件权限"""
|
||||
log_step("🔐 设置文件权限...")
|
||||
cmd = f'ssh {USERNAME}@{SERVER_IP} "chmod -R 755 {REMOTE_PATH}"'
|
||||
success, _, stderr = run_command(cmd)
|
||||
if not success:
|
||||
log_error(f"设置权限失败: {stderr}")
|
||||
|
||||
|
||||
def deploy():
|
||||
"""执行部署"""
|
||||
print("开始部署管理后台应用到服务器...")
|
||||
|
||||
# 检查npm
|
||||
check_npm()
|
||||
|
||||
# 清理旧构建
|
||||
clean_dist()
|
||||
|
||||
# 构建项目
|
||||
build_project()
|
||||
|
||||
# 验证构建结果
|
||||
verify_dist()
|
||||
|
||||
# 创建远程目录
|
||||
create_remote_dir()
|
||||
|
||||
# 上传文件
|
||||
if upload_files():
|
||||
# 设置权限
|
||||
set_permissions()
|
||||
|
||||
log_info("管理后台部署完成!")
|
||||
print(f"📱 访问地址: http://{SERVER_IP}/emotion-museum-admin/")
|
||||
print("🔧 管理后台功能: AI配置管理、用户管理、数据统计等")
|
||||
else:
|
||||
log_error("部署失败,请检查:")
|
||||
print("1. 服务器IP地址是否正确")
|
||||
print("2. SSH密钥是否配置正确")
|
||||
print("3. 服务器目录权限是否正确")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
deploy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -26,5 +26,16 @@ export const menuConfig: MenuItem[] = [
|
||||
path: '/aiconfig',
|
||||
title: 'AI配置管理',
|
||||
icon: 'Setting'
|
||||
},
|
||||
{
|
||||
path: '/tools',
|
||||
title: '开发工具',
|
||||
icon: 'Tools',
|
||||
children: [
|
||||
{
|
||||
path: '/tools/api-tester',
|
||||
title: 'API接口调用'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -63,6 +63,20 @@ const routes: RouteRecordRaw[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/tools',
|
||||
component: Layout,
|
||||
redirect: '/tools/api-tester',
|
||||
meta: { title: '开发工具', icon: 'Tools' },
|
||||
children: [
|
||||
{
|
||||
path: 'api-tester',
|
||||
name: 'ApiTester',
|
||||
component: () => import('@/views/tools/ApiTester.vue'),
|
||||
meta: { title: 'API接口调用' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
<template>
|
||||
<div class="api-tester">
|
||||
<h2 class="page-title">API接口调用</h2>
|
||||
|
||||
<!-- 请求配置区域 -->
|
||||
<el-card class="request-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>请求配置</span>
|
||||
<el-button type="primary" @click="sendRequest" :loading="loading">
|
||||
发送请求
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 请求方法和URL -->
|
||||
<el-form :model="requestForm" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4">
|
||||
<el-form-item label="请求方法">
|
||||
<el-select v-model="requestForm.method" style="width: 100%">
|
||||
<el-option label="GET" value="GET" />
|
||||
<el-option label="POST" value="POST" />
|
||||
<el-option label="PUT" value="PUT" />
|
||||
<el-option label="DELETE" value="DELETE" />
|
||||
<el-option label="PATCH" value="PATCH" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="20">
|
||||
<el-form-item label="接口路径">
|
||||
<el-input
|
||||
v-model="requestForm.url"
|
||||
placeholder="请输入接口路径,如:/api/user-profile/migrateLifeEvents"
|
||||
clearable
|
||||
>
|
||||
<template #prepend>{{ baseUrl }}</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<!-- 参数配置标签页 -->
|
||||
<el-tabs v-model="activeTab" type="border-card">
|
||||
<!-- Query参数 -->
|
||||
<el-tab-pane label="Query参数" name="query">
|
||||
<div class="params-header">
|
||||
<span class="params-tip">URL查询参数(?key=value&key2=value2)</span>
|
||||
<el-button type="primary" size="small" @click="addQueryParam">
|
||||
添加参数
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="requestForm.queryParams" border size="small">
|
||||
<el-table-column label="参数名" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.key" placeholder="参数名" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数值" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.value" placeholder="参数值" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.description" placeholder="描述(可选)" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link size="small" @click="removeQueryParam($index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 请求头 -->
|
||||
<el-tab-pane label="请求头" name="headers">
|
||||
<div class="params-header">
|
||||
<span class="params-tip">自定义请求头(Authorization已自动添加)</span>
|
||||
<el-button type="primary" size="small" @click="addHeader">
|
||||
添加请求头
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="requestForm.headers" border size="small">
|
||||
<el-table-column label="Header名" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.key" placeholder="Header名" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Header值" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.value" placeholder="Header值" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.description" placeholder="描述(可选)" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link size="small" @click="removeHeader($index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 请求体 -->
|
||||
<el-tab-pane label="请求体" name="body">
|
||||
<div class="params-header">
|
||||
<span class="params-tip">请求体内容(JSON格式)</span>
|
||||
<div class="body-actions">
|
||||
<el-button type="success" size="small" @click="formatRequestBody">
|
||||
格式化JSON
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="compressRequestBody">
|
||||
压缩JSON
|
||||
</el-button>
|
||||
<el-button size="small" @click="clearRequestBody">
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="requestForm.body"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder='请输入JSON格式的请求体,如:{"key": "value"}'
|
||||
class="body-textarea"
|
||||
/>
|
||||
<div v-if="bodyError" class="body-error">
|
||||
<el-alert :title="bodyError" type="error" :closable="false" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<!-- 响应结果区域 -->
|
||||
<el-card class="response-card" v-if="responseData">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>响应结果</span>
|
||||
<div class="response-info">
|
||||
<el-tag :type="getStatusType(responseData.status)" size="small">
|
||||
状态码: {{ responseData.status }}
|
||||
</el-tag>
|
||||
<el-tag type="info" size="small">
|
||||
耗时: {{ responseData.duration }}ms
|
||||
</el-tag>
|
||||
<el-button type="primary" size="small" @click="copyResponse">
|
||||
复制响应
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="responseTab" type="border-card">
|
||||
<!-- 响应体 -->
|
||||
<el-tab-pane label="响应体" name="body">
|
||||
<div class="response-body">
|
||||
<pre class="json-display">{{ formatJson(responseData.data) }}</pre>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 响应头 -->
|
||||
<el-tab-pane label="响应头" name="headers">
|
||||
<el-table :data="responseHeaders" border size="small">
|
||||
<el-table-column prop="key" label="Header名" min-width="200" />
|
||||
<el-table-column prop="value" label="Header值" min-width="300" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<!-- 历史记录区域 -->
|
||||
<el-card class="history-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>请求历史(最近10条)</span>
|
||||
<el-button type="danger" size="small" @click="clearHistory" :disabled="history.length === 0">
|
||||
清空历史
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="history" border size="small" v-if="history.length > 0">
|
||||
<el-table-column label="方法" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getMethodType(row.method)" size="small">
|
||||
{{ row.method }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="url" label="接口路径" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="duration" label="耗时" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration }}ms
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="time" label="时间" width="180" />
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="loadFromHistory(row)">
|
||||
加载
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="暂无请求历史" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
/**
|
||||
* 参数项接口
|
||||
*/
|
||||
interface ParamItem {
|
||||
key: string
|
||||
value: string
|
||||
description: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求表单接口
|
||||
*/
|
||||
interface RequestForm {
|
||||
method: string
|
||||
url: string
|
||||
queryParams: ParamItem[]
|
||||
headers: ParamItem[]
|
||||
body: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应数据接口
|
||||
*/
|
||||
interface ResponseData {
|
||||
status: number
|
||||
data: any
|
||||
headers: Record<string, string>
|
||||
duration: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史记录接口
|
||||
*/
|
||||
interface HistoryItem {
|
||||
method: string
|
||||
url: string
|
||||
status: number
|
||||
duration: number
|
||||
time: string
|
||||
queryParams: ParamItem[]
|
||||
headers: ParamItem[]
|
||||
body: string
|
||||
}
|
||||
|
||||
// 基础URL
|
||||
const baseUrl = computed(() => import.meta.env.VITE_APP_BASE_API || '')
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref('query')
|
||||
const responseTab = ref('body')
|
||||
|
||||
// 请求表单
|
||||
const requestForm = reactive<RequestForm>({
|
||||
method: 'GET',
|
||||
url: '',
|
||||
queryParams: [],
|
||||
headers: [],
|
||||
body: ''
|
||||
})
|
||||
|
||||
// 响应数据
|
||||
const responseData = ref<ResponseData | null>(null)
|
||||
|
||||
// 请求体错误信息
|
||||
const bodyError = ref('')
|
||||
|
||||
// 历史记录
|
||||
const history = ref<HistoryItem[]>([])
|
||||
|
||||
// 响应头列表
|
||||
const responseHeaders = computed(() => {
|
||||
if (!responseData.value?.headers) return []
|
||||
return Object.entries(responseData.value.headers).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value)
|
||||
}))
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加Query参数
|
||||
*/
|
||||
const addQueryParam = () => {
|
||||
requestForm.queryParams.push({ key: '', value: '', description: '' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除Query参数
|
||||
*/
|
||||
const removeQueryParam = (index: number) => {
|
||||
requestForm.queryParams.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加请求头
|
||||
*/
|
||||
const addHeader = () => {
|
||||
requestForm.headers.push({ key: '', value: '', description: '' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除请求头
|
||||
*/
|
||||
const removeHeader = (index: number) => {
|
||||
requestForm.headers.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化请求体JSON
|
||||
*/
|
||||
const formatRequestBody = () => {
|
||||
if (!requestForm.body.trim()) return
|
||||
try {
|
||||
const parsed = JSON.parse(requestForm.body)
|
||||
requestForm.body = JSON.stringify(parsed, null, 2)
|
||||
bodyError.value = ''
|
||||
ElMessage.success('格式化成功')
|
||||
} catch (e) {
|
||||
bodyError.value = 'JSON格式错误: ' + (e as Error).message
|
||||
ElMessage.error('JSON格式错误')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩请求体JSON
|
||||
*/
|
||||
const compressRequestBody = () => {
|
||||
if (!requestForm.body.trim()) return
|
||||
try {
|
||||
const parsed = JSON.parse(requestForm.body)
|
||||
requestForm.body = JSON.stringify(parsed)
|
||||
bodyError.value = ''
|
||||
ElMessage.success('压缩成功')
|
||||
} catch (e) {
|
||||
bodyError.value = 'JSON格式错误: ' + (e as Error).message
|
||||
ElMessage.error('JSON格式错误')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空请求体
|
||||
*/
|
||||
const clearRequestBody = () => {
|
||||
requestForm.body = ''
|
||||
bodyError.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化JSON显示
|
||||
*/
|
||||
const formatJson = (data: any): string => {
|
||||
try {
|
||||
return JSON.stringify(data, null, 2)
|
||||
} catch {
|
||||
return String(data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态码对应的标签类型
|
||||
*/
|
||||
const getStatusType = (status: number): string => {
|
||||
if (status >= 200 && status < 300) return 'success'
|
||||
if (status >= 400 && status < 500) return 'warning'
|
||||
if (status >= 500) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求方法对应的标签类型
|
||||
*/
|
||||
const getMethodType = (method: string): string => {
|
||||
const types: Record<string, string> = {
|
||||
GET: 'success',
|
||||
POST: 'primary',
|
||||
PUT: 'warning',
|
||||
DELETE: 'danger',
|
||||
PATCH: 'info'
|
||||
}
|
||||
return types[method] || 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求
|
||||
*/
|
||||
const sendRequest = async () => {
|
||||
if (!requestForm.url.trim()) {
|
||||
ElMessage.warning('请输入接口路径')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求体JSON格式
|
||||
if (requestForm.body.trim() && ['POST', 'PUT', 'PATCH'].includes(requestForm.method)) {
|
||||
try {
|
||||
JSON.parse(requestForm.body)
|
||||
bodyError.value = ''
|
||||
} catch (e) {
|
||||
bodyError.value = 'JSON格式错误: ' + (e as Error).message
|
||||
ElMessage.error('请求体JSON格式错误')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
// 获取token
|
||||
const token = localStorage.getItem('adminToken')
|
||||
|
||||
// 构建请求头
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
// 添加自定义请求头
|
||||
requestForm.headers.forEach(h => {
|
||||
if (h.key.trim()) {
|
||||
headers[h.key] = h.value
|
||||
}
|
||||
})
|
||||
|
||||
// 构建Query参数
|
||||
const params: Record<string, string> = {}
|
||||
requestForm.queryParams.forEach(p => {
|
||||
if (p.key.trim()) {
|
||||
params[p.key] = p.value
|
||||
}
|
||||
})
|
||||
|
||||
// 构建请求体
|
||||
let data = undefined
|
||||
if (requestForm.body.trim() && ['POST', 'PUT', 'PATCH'].includes(requestForm.method)) {
|
||||
data = JSON.parse(requestForm.body)
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const response = await axios({
|
||||
method: requestForm.method.toLowerCase(),
|
||||
url: baseUrl.value + requestForm.url,
|
||||
headers,
|
||||
params,
|
||||
data,
|
||||
validateStatus: () => true // 不抛出HTTP错误
|
||||
})
|
||||
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
// 保存响应数据
|
||||
responseData.value = {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
headers: response.headers as Record<string, string>,
|
||||
duration
|
||||
}
|
||||
|
||||
// 添加到历史记录
|
||||
addToHistory({
|
||||
method: requestForm.method,
|
||||
url: requestForm.url,
|
||||
status: response.status,
|
||||
duration,
|
||||
time: new Date().toLocaleString(),
|
||||
queryParams: [...requestForm.queryParams],
|
||||
headers: [...requestForm.headers],
|
||||
body: requestForm.body
|
||||
})
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
ElMessage.success(`请求成功 (${response.status})`)
|
||||
} else {
|
||||
ElMessage.warning(`请求完成,状态码: ${response.status}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
const duration = Date.now() - startTime
|
||||
responseData.value = {
|
||||
status: 0,
|
||||
data: { error: error.message || '请求失败' },
|
||||
headers: {},
|
||||
duration
|
||||
}
|
||||
ElMessage.error('请求失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到历史记录
|
||||
*/
|
||||
const addToHistory = (item: HistoryItem) => {
|
||||
history.value.unshift(item)
|
||||
// 只保留最近10条
|
||||
if (history.value.length > 10) {
|
||||
history.value = history.value.slice(0, 10)
|
||||
}
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('apiTesterHistory', JSON.stringify(history.value))
|
||||
}
|
||||
|
||||
/**
|
||||
* 从历史记录加载
|
||||
*/
|
||||
const loadFromHistory = (item: HistoryItem) => {
|
||||
requestForm.method = item.method
|
||||
requestForm.url = item.url
|
||||
requestForm.queryParams = item.queryParams ? [...item.queryParams] : []
|
||||
requestForm.headers = item.headers ? [...item.headers] : []
|
||||
requestForm.body = item.body || ''
|
||||
ElMessage.success('已加载历史请求')
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空历史记录
|
||||
*/
|
||||
const clearHistory = () => {
|
||||
history.value = []
|
||||
localStorage.removeItem('apiTesterHistory')
|
||||
ElMessage.success('历史记录已清空')
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制响应内容
|
||||
*/
|
||||
const copyResponse = async () => {
|
||||
if (!responseData.value) return
|
||||
try {
|
||||
const text = formatJson(responseData.value.data)
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时加载历史记录
|
||||
const initHistory = () => {
|
||||
const saved = localStorage.getItem('apiTesterHistory')
|
||||
if (saved) {
|
||||
try {
|
||||
history.value = JSON.parse(saved)
|
||||
} catch {
|
||||
history.value = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initHistory()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.api-tester {
|
||||
.page-title {
|
||||
margin-bottom: 20px;
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.request-card,
|
||||
.response-card,
|
||||
.history-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.params-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.params-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.body-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.body-textarea {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.body-error {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.response-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.response-body {
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.json-display {
|
||||
margin: 0;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user