good version for 算法注册
This commit is contained in:
117
frontend/public/test_algorithm_api.html
Normal file
117
frontend/public/test_algorithm_api.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>测试算法API</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.success {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
.info {
|
||||
color: blue;
|
||||
}
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>测试算法API</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. 测试GET /api/algorithms</h2>
|
||||
<button onclick="testAlgorithmsApi()">测试算法API</button>
|
||||
<div id="algorithmsResult"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. 测试GET /api/v1/algorithms</h2>
|
||||
<button onclick="testAlgorithmsApiV1()">测试算法API V1</button>
|
||||
<div id="algorithmsResultV1"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function testAlgorithmsApi() {
|
||||
const resultDiv = document.getElementById('algorithmsResult');
|
||||
resultDiv.innerHTML = '<p class="info">正在测试...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/algorithms', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = '<p class="success">✅ 算法API调用成功!</p>';
|
||||
resultDiv.innerHTML += '<p>算法数量: ' + data.algorithms.length + '</p>';
|
||||
resultDiv.innerHTML += '<pre>' + JSON.stringify(data.algorithms.map(a => ({name: a.name, id: a.id})), null, 2) + '</pre>';
|
||||
} else {
|
||||
resultDiv.innerHTML = '<p class="error">❌ 算法API调用失败!</p>';
|
||||
resultDiv.innerHTML += '<p>状态码: ' + response.status + '</p>';
|
||||
resultDiv.innerHTML += '<p>错误: ' + JSON.stringify(data) + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = '<p class="error">❌ 算法API请求失败!</p>';
|
||||
resultDiv.innerHTML += '<p>错误: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function testAlgorithmsApiV1() {
|
||||
const resultDiv = document.getElementById('algorithmsResultV1');
|
||||
resultDiv.innerHTML = '<p class="info">正在测试...</p>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/algorithms', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = '<p class="success">✅ 算法API V1调用成功!</p>';
|
||||
resultDiv.innerHTML += '<p>算法数量: ' + data.algorithms.length + '</p>';
|
||||
resultDiv.innerHTML += '<pre>' + JSON.stringify(data.algorithms.map(a => ({name: a.name, id: a.id})), null, 2) + '</pre>';
|
||||
} else {
|
||||
resultDiv.innerHTML = '<p class="error">❌ 算法API V1调用失败!</p>';
|
||||
resultDiv.innerHTML += '<p>状态码: ' + response.status + '</p>';
|
||||
resultDiv.innerHTML += '<p>错误: ' + JSON.stringify(data) + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = '<p class="error">❌ 算法API V1请求失败!</p>';
|
||||
resultDiv.innerHTML += '<p>错误: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,8 +13,8 @@
|
||||
<el-menu-item index="/">
|
||||
<router-link to="/">首页</router-link>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/algorithms">
|
||||
<router-link to="/algorithms">算法列表</router-link>
|
||||
<el-menu-item v-if="userStore.isLoggedIn" index="/algorithms">
|
||||
<router-link to="/algorithms">算法展示平台</router-link>
|
||||
</el-menu-item>
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/admin">
|
||||
<router-link to="/admin">管理员中心</router-link>
|
||||
@@ -84,6 +84,14 @@ const handleLogout = () => {
|
||||
// 初始化用户状态
|
||||
onMounted(() => {
|
||||
userStore.init()
|
||||
|
||||
// 调试信息
|
||||
console.log('App.vue - userStore初始化完成')
|
||||
console.log('isLoggedIn:', userStore.isLoggedIn)
|
||||
console.log('user:', userStore.user)
|
||||
console.log('user.role?.name:', userStore.user?.role?.name)
|
||||
console.log('user.role_name:', userStore.user?.role_name)
|
||||
console.log('isAdmin:', userStore.isAdmin)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import { ElMessage } from 'element-plus'
|
||||
// 配置axios
|
||||
// 移除baseURL配置,使用Vite代理处理API路径映射
|
||||
axios.defaults.headers.common['Content-Type'] = 'application/json'
|
||||
axios.defaults.withCredentials = true
|
||||
|
||||
// 添加请求拦截器,用于添加认证token和调试日志
|
||||
axios.interceptors.request.use(
|
||||
@@ -39,11 +38,18 @@ axios.interceptors.request.use(
|
||||
console.log('发送请求:', config.method?.toUpperCase(), config.url)
|
||||
console.log('完整URL:', (axios.defaults.baseURL || '') + (config.url || ''))
|
||||
|
||||
// 添加认证token
|
||||
// 添加认证token(登录请求除外)
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
console.log('请求拦截器 - localStorage中的token:', token ? token.substring(0, 50) + '...' : 'null')
|
||||
|
||||
// 如果是登录请求,不添加token
|
||||
if (token && !config.url?.includes('/login')) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
console.log('请求拦截器 - 已添加Authorization头:', config.headers.Authorization.substring(0, 50) + '...')
|
||||
} else {
|
||||
console.log('请求拦截器 - 未添加Authorization头')
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
@@ -61,9 +67,39 @@ axios.interceptors.response.use(
|
||||
console.error('请求错误:', error)
|
||||
console.error('错误状态:', error.response?.status)
|
||||
console.error('错误数据:', error.response?.data)
|
||||
console.error('请求URL:', error.config?.url)
|
||||
|
||||
// 处理401错误,跳转到登录页
|
||||
// 处理401错误
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 如果是登录请求失败,不显示"登录已过期"的错误
|
||||
if (error.config?.url?.includes('/login')) {
|
||||
// 登录失败,让登录页面自己处理错误
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 如果是获取用户信息失败,清除token但不显示错误消息
|
||||
if (error.config?.url?.includes('/users/me')) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 如果是Gitea配置请求失败,不显示错误消息
|
||||
if (error.config?.url?.includes('/gitea/config')) {
|
||||
// Gitea配置可能不存在,不显示错误
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 如果是仓库列表请求失败,不显示错误消息并记录详细日志
|
||||
if (error.config?.url?.includes('/repositories')) {
|
||||
console.error('仓库列表请求失败:', error)
|
||||
console.error('错误详情:', error.response?.data)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// 其他401错误,跳转到登录页
|
||||
console.error('401错误 - 请求URL:', error.config?.url)
|
||||
console.error('401错误 - 响应数据:', error.response?.data)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
|
||||
@@ -16,7 +16,8 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'Algorithms',
|
||||
component: () => import('../views/AlgorithmsView.vue'),
|
||||
meta: {
|
||||
title: '算法列表'
|
||||
title: '算法展示平台',
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -93,6 +94,22 @@ const routes: Array<RouteRecordRaw> = [
|
||||
meta: {
|
||||
title: '服务注册'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
name: 'AdminConfigManagement',
|
||||
component: () => import('../views/admin/AdminConfigManagementView.vue'),
|
||||
meta: {
|
||||
title: '配置管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'comparison',
|
||||
name: 'AdminAlgorithmComparison',
|
||||
component: () => import('../views/admin/AdminAlgorithmComparisonView.vue'),
|
||||
meta: {
|
||||
title: '算法效果比较'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
141
frontend/src/services/admin.ts
Normal file
141
frontend/src/services/admin.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export interface ConfigItem {
|
||||
id: string
|
||||
config_key: string
|
||||
config_value: string
|
||||
config_type: string
|
||||
service_id: string | null
|
||||
description: string
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AlgorithmConfig {
|
||||
algorithm_id: string
|
||||
algorithm_name: string
|
||||
version: string
|
||||
config: string
|
||||
}
|
||||
|
||||
export interface ComparisonResult {
|
||||
success: boolean
|
||||
comparison_time: string
|
||||
results: Array<{
|
||||
algorithm_name: string
|
||||
algorithm_id: string
|
||||
version: string
|
||||
execution_time: number
|
||||
success: boolean
|
||||
output: any
|
||||
error: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ComparisonReport {
|
||||
success: boolean
|
||||
report_time: string
|
||||
summary: string
|
||||
performance_analysis: string
|
||||
recommendations: string
|
||||
}
|
||||
|
||||
class ConfigService {
|
||||
private readonly baseUrl = '/api/config'
|
||||
|
||||
async getConfig(configKey: string): Promise<ConfigItem | null> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseUrl}/${configKey}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async setConfig(configKey: string, configData: {
|
||||
value: string
|
||||
type?: string
|
||||
service_id?: string | null
|
||||
description?: string
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseUrl}/${configKey}`, {
|
||||
value: configData.value,
|
||||
type: configData.type || 'system',
|
||||
service_id: configData.service_id || null,
|
||||
description: configData.description || ''
|
||||
})
|
||||
return response.data.message === '设置配置成功'
|
||||
} catch (error) {
|
||||
console.error('设置配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getServiceConfigs(serviceId: string): Promise<ConfigItem[]> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseUrl}/service/${serviceId}`)
|
||||
return response.data.configs || []
|
||||
} catch (error) {
|
||||
console.error('获取服务配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteConfig(configKey: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await axios.delete(`${this.baseUrl}/${configKey}`)
|
||||
return response.data.message === '删除配置成功'
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getAllConfigs(configType?: string): Promise<ConfigItem[]> {
|
||||
try {
|
||||
const params: Record<string, any> = {}
|
||||
if (configType) {
|
||||
params.config_type = configType
|
||||
}
|
||||
|
||||
const response = await axios.get(`${this.baseUrl}/`, { params })
|
||||
return response.data.configs || []
|
||||
} catch (error) {
|
||||
console.error('获取所有配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ComparisonService {
|
||||
private readonly baseUrl = '/api/comparison'
|
||||
|
||||
async compareAlgorithms(inputData: any, algorithmConfigs: AlgorithmConfig[]): Promise<ComparisonResult> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseUrl}/compare-algorithms`, {
|
||||
input_data: inputData,
|
||||
algorithm_configs: algorithmConfigs
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('算法比较失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async generateComparisonReport(comparisonResults: ComparisonResult): Promise<ComparisonReport> {
|
||||
try {
|
||||
const response = await axios.post(`${this.baseUrl}/generate-report`, comparisonResults)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('生成比较报告失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const configService = new ConfigService()
|
||||
export const comparisonService = new ComparisonService()
|
||||
224
frontend/src/services/apiManagement.ts
Normal file
224
frontend/src/services/apiManagement.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const BASE_URL = 'http://0.0.0.0:8001/api/v1/api-management'
|
||||
|
||||
export interface ApiEndpoint {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
path: string
|
||||
method: string
|
||||
algorithm_id: string
|
||||
algorithm_name: string
|
||||
version_id: string
|
||||
version: string
|
||||
service_id: string | null
|
||||
status: string
|
||||
is_public: boolean
|
||||
call_count: string
|
||||
success_count: string
|
||||
error_count: string
|
||||
avg_response_time: string
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
last_called_at: string | null
|
||||
}
|
||||
|
||||
export interface ApiStats {
|
||||
total_endpoints: number
|
||||
active_endpoints: number
|
||||
total_calls: string
|
||||
total_success: string
|
||||
total_errors: string
|
||||
avg_response_time: string
|
||||
}
|
||||
|
||||
export const apiManagementService = {
|
||||
async getApiEndpoints(algorithmId?: string, status?: string): Promise<{ endpoints: ApiEndpoint[], total: number }> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (algorithmId) params.append('algorithm_id', algorithmId)
|
||||
if (status) params.append('status', status)
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取API端点列表失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async getApiEndpoint(endpointId: string): Promise<ApiEndpoint> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints/${endpointId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取API端点详情失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async createApiEndpoint(data: {
|
||||
name: string
|
||||
description: string
|
||||
path: string
|
||||
method: string
|
||||
algorithm_id: string
|
||||
version_id: string
|
||||
service_id?: string
|
||||
requires_auth: boolean
|
||||
allowed_roles: string[]
|
||||
rate_limit?: Record<string, any>
|
||||
is_public: boolean
|
||||
config: Record<string, any>
|
||||
}): Promise<ApiEndpoint> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || '创建API端点失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async updateApiEndpoint(endpointId: string, data: Partial<{
|
||||
name: string
|
||||
description: string
|
||||
path: string
|
||||
method: string
|
||||
requires_auth: boolean
|
||||
allowed_roles: string[]
|
||||
rate_limit?: Record<string, any>
|
||||
is_public: boolean
|
||||
config: Record<string, any>
|
||||
status: string
|
||||
}>): Promise<ApiEndpoint> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints/${endpointId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || '更新API端点失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async deleteApiEndpoint(endpointId: string): Promise<{ success: boolean; message: string }> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints/${endpointId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || '删除API端点失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async getApiStats(): Promise<ApiStats> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/stats`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取API统计信息失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
},
|
||||
|
||||
async testApiEndpoint(endpointId: string, payload: Record<string, any>): Promise<{
|
||||
success: boolean
|
||||
result?: any
|
||||
response_time: number
|
||||
message?: string
|
||||
error?: string
|
||||
}> {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
throw new Error('未登录')
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/endpoints/${endpointId}/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.detail || '测试API端点失败')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
}
|
||||
94
frontend/src/services/serviceManagement.ts
Normal file
94
frontend/src/services/serviceManagement.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export interface Service {
|
||||
id: string
|
||||
service_id: string
|
||||
name: string
|
||||
algorithm_name: string
|
||||
version: string
|
||||
host: string
|
||||
port: number
|
||||
api_url: string
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface ServiceOperationResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
service_id: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface ServiceListResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
services: Service[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface RegisterServiceRequest {
|
||||
repository_id: string
|
||||
name: string
|
||||
version: string
|
||||
service_type: string
|
||||
host: string
|
||||
port: number
|
||||
timeout: number
|
||||
health_check_path: string
|
||||
environment: Record<string, any>
|
||||
}
|
||||
|
||||
export interface RegisterServiceResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
service: Service
|
||||
}
|
||||
|
||||
export interface DeleteServiceResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
service_id: string
|
||||
}
|
||||
|
||||
const BASE_URL = '/api/v1/services'
|
||||
|
||||
export const serviceManagementApi = {
|
||||
getServices: async (): Promise<ServiceListResponse> => {
|
||||
const response = await axios.get<ServiceListResponse>(BASE_URL)
|
||||
return response.data
|
||||
},
|
||||
|
||||
registerService: async (data: RegisterServiceRequest): Promise<RegisterServiceResponse> => {
|
||||
const response = await axios.post<RegisterServiceResponse>(`${BASE_URL}/register`, data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
startService: async (serviceId: string): Promise<ServiceOperationResponse> => {
|
||||
const response = await axios.post<ServiceOperationResponse>(`${BASE_URL}/${serviceId}/start`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
stopService: async (serviceId: string): Promise<ServiceOperationResponse> => {
|
||||
const response = await axios.post<ServiceOperationResponse>(`${BASE_URL}/${serviceId}/stop`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
restartService: async (serviceId: string): Promise<ServiceOperationResponse> => {
|
||||
const response = await axios.post<ServiceOperationResponse>(`${BASE_URL}/${serviceId}/restart`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
deleteService: async (serviceId: string): Promise<DeleteServiceResponse> => {
|
||||
const response = await axios.delete<DeleteServiceResponse>(`${BASE_URL}/${serviceId}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
getServiceDetail: async (serviceId: string): Promise<Service> => {
|
||||
const response = await axios.get<{ success: boolean; message: string; service: Service }>(`${BASE_URL}/${serviceId}`)
|
||||
return response.data.service
|
||||
}
|
||||
}
|
||||
|
||||
export default serviceManagementApi
|
||||
@@ -7,6 +7,8 @@ interface Algorithm {
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
tech_category: string // 技术分类:计算机视觉、视频处理、自然语言处理等
|
||||
output_type: string // 输出类型:图片、视频、文本、JSON等
|
||||
status: string
|
||||
versions: AlgorithmVersion[]
|
||||
created_at: string
|
||||
@@ -70,7 +72,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
|
||||
try {
|
||||
const params = type ? { type } : {}
|
||||
const response = await axios.get('/algorithms', { params })
|
||||
const response = await axios.get('/api/algorithms', { params })
|
||||
this.algorithms = response.data.algorithms
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -87,7 +89,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/${id}`)
|
||||
const response = await axios.get(`/api/algorithms/${id}`)
|
||||
this.currentAlgorithm = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -104,7 +106,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/${algorithmId}/versions`)
|
||||
const response = await axios.get(`/api/algorithms/${algorithmId}/versions`)
|
||||
if (this.currentAlgorithm) {
|
||||
this.currentAlgorithm.versions = response.data
|
||||
}
|
||||
@@ -123,7 +125,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/algorithms/call', request)
|
||||
const response = await axios.post('/api/algorithms/call', request)
|
||||
this.callResult = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -140,7 +142,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/calls/${callId}`)
|
||||
const response = await axios.get(`/api/algorithms/calls/${callId}`)
|
||||
this.callResult = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -157,7 +159,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/openai/generate-data', { prompt, data_type: dataType })
|
||||
const response = await axios.post('/api/openai/generate-data', { prompt, data_type: dataType })
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '生成仿真数据失败'
|
||||
@@ -173,7 +175,7 @@ export const useAlgorithmStore = defineStore('algorithm', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/algorithms', algorithmData)
|
||||
const response = await axios.post('/api/algorithms', algorithmData)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '创建算法失败'
|
||||
|
||||
@@ -15,6 +15,7 @@ interface User {
|
||||
email: string
|
||||
role_id: string
|
||||
role?: Role
|
||||
role_name?: string
|
||||
status: string
|
||||
}
|
||||
|
||||
@@ -43,7 +44,7 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token,
|
||||
isAdmin: (state) => state.user?.role?.name === 'admin'
|
||||
isAdmin: (state) => state.user?.role?.name === 'admin' || state.user?.role_name === 'admin'
|
||||
},
|
||||
|
||||
actions: {
|
||||
@@ -54,16 +55,27 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
try {
|
||||
console.log('开始登录请求...', credentials)
|
||||
console.log('登录前的token:', this.token ? this.token.substring(0, 50) + '...' : 'null')
|
||||
console.log('登录前的localStorage:', {
|
||||
token: localStorage.getItem('token')?.substring(0, 50) + '...',
|
||||
user: localStorage.getItem('user')
|
||||
})
|
||||
|
||||
const response = await axios.post('/api/users/login', credentials)
|
||||
console.log('登录响应:', response.data)
|
||||
const { access_token } = response.data
|
||||
|
||||
// 保存token到本地存储
|
||||
// 保存token到本地存储(这一步必须在fetchUser之前)
|
||||
localStorage.setItem('token', access_token)
|
||||
this.token = access_token
|
||||
|
||||
// 获取用户信息
|
||||
await this.fetchUser()
|
||||
// 获取用户信息(即使失败也不影响登录)
|
||||
try {
|
||||
await this.fetchUser()
|
||||
} catch (error) {
|
||||
console.warn('获取用户信息失败,但登录已成功:', error)
|
||||
// fetchUser失败不影响登录,用户信息可以在后续请求中获取
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
@@ -113,14 +125,21 @@ export const useUserStore = defineStore('user', {
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
console.log('fetchUser - 开始获取用户信息')
|
||||
console.log('fetchUser - 当前token:', this.token ? this.token.substring(0, 50) + '...' : 'null')
|
||||
console.log('fetchUser - localStorage中的token:', localStorage.getItem('token')?.substring(0, 50) + '...')
|
||||
|
||||
const response = await axios.get('/api/users/me')
|
||||
console.log('fetchUser - 获取用户信息成功:', response.data)
|
||||
this.user = response.data
|
||||
|
||||
// 保存用户信息到本地存储
|
||||
localStorage.setItem('user', JSON.stringify(response.data))
|
||||
console.log('fetchUser - 用户信息已保存到localStorage')
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error('fetchUser - 获取用户信息失败:', error)
|
||||
this.error = error.response?.data?.detail || '获取用户信息失败'
|
||||
return false
|
||||
} finally {
|
||||
|
||||
@@ -38,6 +38,18 @@
|
||||
</template>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/config">
|
||||
<template #icon>
|
||||
<el-icon><setting /></el-icon>
|
||||
</template>
|
||||
<span>配置管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/comparison">
|
||||
<template #icon>
|
||||
<el-icon><trend-charts /></el-icon>
|
||||
</template>
|
||||
<span>算法效果比较</span>
|
||||
</el-menu-item>
|
||||
|
||||
</el-menu>
|
||||
</aside>
|
||||
@@ -57,7 +69,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { DataAnalysis, User, Link, Cpu } from '@element-plus/icons-vue'
|
||||
import { DataAnalysis, User, Link, Cpu, Setting, TrendCharts } from '@element-plus/icons-vue'
|
||||
|
||||
// 获取路由和路由器
|
||||
const route = useRoute()
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
<template>
|
||||
<div class="algorithms-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>算法列表</h1>
|
||||
<h1>算法能力展示</h1>
|
||||
<p class="subtitle">探索我们强大的AI算法能力,支持多种应用场景</p>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" class="filter-form">
|
||||
<el-form-item label="算法类型">
|
||||
<el-select v-model="selectedType" placeholder="选择算法类型" @change="handleTypeChange">
|
||||
<el-form-item label="技术分类">
|
||||
<el-select v-model="selectedTechCategory" placeholder="选择技术分类" @change="handleTypeChange">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="边缘计算" value="edge_computing" />
|
||||
<el-option label="医疗算法" value="medical" />
|
||||
<el-option label="计算机视觉" value="computer_vision" />
|
||||
<el-option label="视频处理" value="video_processing" />
|
||||
<el-option label="自然语言处理" value="nlp" />
|
||||
<el-option label="机器学习" value="ml" />
|
||||
<el-option label="强化学习" value="reinforcement_learning" />
|
||||
<el-option label="边缘计算" value="edge_computing" />
|
||||
<el-option label="医疗算法" value="medical" />
|
||||
<el-option label="自动驾驶算法" value="autonomous_driving" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="输出类型">
|
||||
<el-select v-model="selectedOutputType" placeholder="选择输出类型" @change="handleTypeChange">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="图片" value="image" />
|
||||
<el-option label="视频" value="video" />
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="JSON" value="json" />
|
||||
<el-option label="音频" value="audio" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="搜索">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
@@ -46,7 +57,10 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>{{ algorithm.name }}</h2>
|
||||
<span class="algorithm-type">{{ getAlgorithmTypeName(algorithm.type) }}</span>
|
||||
<div class="tags">
|
||||
<el-tag type="primary" size="small">{{ getTechCategoryName(algorithm.tech_category) }}</el-tag>
|
||||
<el-tag type="success" size="small">{{ getOutputTypeName(algorithm.output_type) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -69,8 +83,9 @@
|
||||
<el-button type="primary" size="small">查看详情</el-button>
|
||||
</router-link>
|
||||
<router-link :to="`/algorithm/${algorithm.id}/call`" class="call-btn">
|
||||
<el-button type="success" size="small">立即调用</el-button>
|
||||
<el-button type="success" size="small">在线演示</el-button>
|
||||
</router-link>
|
||||
<el-button type="info" size="small" @click="showApiDocs(algorithm)">API文档</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -87,46 +102,115 @@
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- API文档对话框 -->
|
||||
<el-dialog
|
||||
v-model="showApiDialog"
|
||||
title="API文档"
|
||||
width="60%"
|
||||
>
|
||||
<div v-if="selectedAlgorithm" class="api-docs">
|
||||
<h3>{{ selectedAlgorithm.name }}</h3>
|
||||
<p>{{ selectedAlgorithm.description }}</p>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>API端点</h4>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="基础URL">
|
||||
http://0.0.0.0:8001/api/v1
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="算法ID">
|
||||
{{ selectedAlgorithm.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="技术分类">
|
||||
{{ getTechCategoryName(selectedAlgorithm.tech_category) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="输出类型">
|
||||
{{ getOutputTypeName(selectedAlgorithm.output_type) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>调用示例</h4>
|
||||
<pre class="code-example">
|
||||
POST /api/v1/algorithms/{{ selectedAlgorithm.id }}/call
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"input_data": {
|
||||
"data": "your input data"
|
||||
},
|
||||
"params": {}
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAlgorithmStore } from '../stores/algorithm'
|
||||
import { Search, Time, Folder, Document } from '@element-plus/icons-vue'
|
||||
import { Search, Timer, Folder, Document } from '@element-plus/icons-vue'
|
||||
|
||||
// 获取算法存储
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 筛选和分页
|
||||
const selectedType = ref('')
|
||||
const selectedTechCategory = ref('')
|
||||
const selectedOutputType = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
// 算法类型映射
|
||||
const algorithmTypes = {
|
||||
// API文档对话框
|
||||
const showApiDialog = ref(false)
|
||||
const selectedAlgorithm = ref<any>(null)
|
||||
|
||||
// 技术分类映射
|
||||
const techCategories = {
|
||||
computer_vision: '计算机视觉',
|
||||
video_processing: '视频处理',
|
||||
nlp: '自然语言处理',
|
||||
ml: '机器学习',
|
||||
reinforcement_learning: '强化学习',
|
||||
edge_computing: '边缘计算',
|
||||
medical: '医疗算法',
|
||||
autonomous_driving: '自动驾驶算法'
|
||||
}
|
||||
|
||||
// 获取算法类型的中文名称
|
||||
const getAlgorithmTypeName = (type: string) => {
|
||||
return algorithmTypes[type as keyof typeof algorithmTypes] || type
|
||||
// 输出类型映射
|
||||
const outputTypes = {
|
||||
image: '图片',
|
||||
video: '视频',
|
||||
text: '文本',
|
||||
json: 'JSON',
|
||||
audio: '音频'
|
||||
}
|
||||
|
||||
// 获取技术分类的中文名称
|
||||
const getTechCategoryName = (type: string) => {
|
||||
return techCategories[type as keyof typeof techCategories] || type
|
||||
}
|
||||
|
||||
// 获取输出类型的中文名称
|
||||
const getOutputTypeName = (type: string) => {
|
||||
return outputTypes[type as keyof typeof outputTypes] || type
|
||||
}
|
||||
|
||||
// 筛选算法
|
||||
const filteredAlgorithms = computed(() => {
|
||||
let algorithms = algorithmStore.algorithms
|
||||
|
||||
// 按类型筛选
|
||||
if (selectedType.value) {
|
||||
algorithms = algorithms.filter(algorithm => algorithm.type === selectedType.value)
|
||||
// 按技术分类筛选
|
||||
if (selectedTechCategory.value) {
|
||||
algorithms = algorithms.filter(algorithm => algorithm.tech_category === selectedTechCategory.value)
|
||||
}
|
||||
|
||||
// 按输出类型筛选
|
||||
if (selectedOutputType.value) {
|
||||
algorithms = algorithms.filter(algorithm => algorithm.output_type === selectedOutputType.value)
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
@@ -159,6 +243,12 @@ const handlePageChange = (page: number) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 显示API文档
|
||||
const showApiDocs = (algorithm: any) => {
|
||||
selectedAlgorithm.value = algorithm
|
||||
showApiDialog.value = true
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
@@ -175,12 +265,21 @@ onMounted(async () => {
|
||||
.algorithms-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.algorithms-container h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
font-size: 32px;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
@@ -195,6 +294,20 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-form .el-form-item {
|
||||
margin-bottom: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filter-form .el-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.filter-form .el-input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.algorithms-list {
|
||||
@@ -215,31 +328,39 @@ onMounted(async () => {
|
||||
.algorithm-card {
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.algorithm-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.algorithm-type {
|
||||
.tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tags .el-tag {
|
||||
padding: 4px 12px;
|
||||
background-color: #ecf5ff;
|
||||
color: #409EFF;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@@ -248,8 +369,9 @@ onMounted(async () => {
|
||||
|
||||
.algorithm-description {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.algorithm-meta {
|
||||
@@ -258,6 +380,7 @@ onMounted(async () => {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
@@ -270,6 +393,11 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-actions a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
@@ -278,6 +406,30 @@ onMounted(async () => {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.api-docs h3 {
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.api-docs h4 {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.api-docs p {
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
background-color: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-form {
|
||||
flex-direction: column;
|
||||
@@ -293,8 +445,14 @@ onMounted(async () => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-actions a {
|
||||
.card-actions a,
|
||||
.card-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,25 +59,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 技术栈 -->
|
||||
<section class="tech-stack-section">
|
||||
<h2>技术栈</h2>
|
||||
<div class="tech-grid">
|
||||
<div class="tech-item">
|
||||
<h3>前端</h3>
|
||||
<p>Vue 3 + TypeScript + Vite + Pinia + Element Plus</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h3>后端</h3>
|
||||
<p>FastAPI + SQLAlchemy + PostgreSQL + Redis + MinIO</p>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<h3>部署</h3>
|
||||
<p>Docker + Docker Compose</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -96,15 +77,15 @@ const popularAlgorithms = ref([
|
||||
},
|
||||
{
|
||||
id: 'algorithm-002',
|
||||
name: '文本情感分析算法',
|
||||
type: 'nlp',
|
||||
description: '分析文本的情感倾向,支持积极、消极、中性三种情感分类'
|
||||
name: '目标识别',
|
||||
type: 'computer_vision',
|
||||
description: '识别图像中的物体位置和类别,支持人脸、车辆、物品等多种目标检测'
|
||||
},
|
||||
{
|
||||
id: 'algorithm-003',
|
||||
name: '推荐算法',
|
||||
type: 'ml',
|
||||
description: '基于用户行为的推荐算法,提供个性化推荐结果'
|
||||
name: '视频分析',
|
||||
type: 'video_processing',
|
||||
description: '分析视频内容,提取关键帧、识别动作、追踪物体等'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -341,43 +322,6 @@ const popularAlgorithms = ref([
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
|
||||
/* 技术栈区域 */
|
||||
.tech-stack-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.tech-stack-section h2 {
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
margin-bottom: 40px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.tech-item {
|
||||
background-color: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tech-item h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tech-item p {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.hero-content h1 {
|
||||
@@ -395,8 +339,7 @@ const popularAlgorithms = ref([
|
||||
}
|
||||
|
||||
.features-grid,
|
||||
.algorithms-grid,
|
||||
.tech-grid {
|
||||
.algorithms-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
619
frontend/src/views/admin/AdminAlgorithmComparisonView.vue
Normal file
619
frontend/src/views/admin/AdminAlgorithmComparisonView.vue
Normal file
@@ -0,0 +1,619 @@
|
||||
<template>
|
||||
<div class="algorithm-comparison-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>算法效果比较</h1>
|
||||
|
||||
<!-- 比较配置区域 -->
|
||||
<el-card class="comparison-config-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>比较配置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 输入数据配置 -->
|
||||
<el-form :model="comparisonForm" label-width="120px">
|
||||
<el-form-item label="输入数据">
|
||||
<el-input
|
||||
v-model="comparisonForm.input_data"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入测试数据(JSON格式)"
|
||||
/>
|
||||
<div class="hint">
|
||||
提示:输入JSON格式的测试数据,例如:{"text": "这是一段测试文本"}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 算法配置列表 -->
|
||||
<el-form-item label="选择算法">
|
||||
<div class="algorithm-configs">
|
||||
<div
|
||||
v-for="(config, index) in comparisonForm.algorithm_configs"
|
||||
:key="index"
|
||||
class="algorithm-config-item"
|
||||
>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="config-header">
|
||||
<span>算法 {{ index + 1 }}</span>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeAlgorithmConfig(index)"
|
||||
:disabled="comparisonForm.algorithm_configs.length <= 2"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="config" label-width="100px">
|
||||
<el-form-item label="算法ID">
|
||||
<el-input
|
||||
v-model="config.algorithm_id"
|
||||
placeholder="请输入算法ID"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="算法名称">
|
||||
<el-input
|
||||
v-model="config.algorithm_name"
|
||||
placeholder="请输入算法名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="版本">
|
||||
<el-input
|
||||
v-model="config.version"
|
||||
placeholder="请输入版本号"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置参数">
|
||||
<el-input
|
||||
v-model="config.config"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置参数(JSON格式)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="addAlgorithmConfig"
|
||||
class="add-algorithm-btn"
|
||||
>
|
||||
<el-icon><plus /></el-icon>
|
||||
添加算法
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" @click="compareAlgorithms" :loading="comparing">
|
||||
<el-icon><loading v-if="comparing" /></el-icon>
|
||||
开始比较
|
||||
</el-button>
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 比较结果区域 -->
|
||||
<el-card v-if="comparisonResult" class="comparison-result-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>比较结果</span>
|
||||
<el-button type="success" @click="generateReport">
|
||||
<el-icon><document /></el-icon>
|
||||
生成报告
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 结果概览 -->
|
||||
<div class="result-overview">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="比较时间">
|
||||
{{ formatDate(comparisonResult.comparison_time) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="算法数量">
|
||||
{{ comparisonResult.results?.length || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="comparisonResult.success ? 'success' : 'danger'">
|
||||
{{ comparisonResult.success ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 详细结果 -->
|
||||
<div class="detailed-results">
|
||||
<h3>详细结果</h3>
|
||||
<el-table :data="comparisonResult.results" style="width: 100%">
|
||||
<el-table-column prop="algorithm_name" label="算法名称" width="150" />
|
||||
<el-table-column prop="algorithm_id" label="算法ID" width="200" />
|
||||
<el-table-column prop="version" label="版本" width="120" />
|
||||
<el-table-column prop="execution_time" label="执行时间(ms)" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.execution_time?.toFixed(2) || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="success" label="执行状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.success ? 'success' : 'danger'">
|
||||
{{ scope.row.success ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="output" label="输出结果" min-width="200">
|
||||
<template #default="scope">
|
||||
<div class="output-cell">
|
||||
{{ formatOutput(scope.row.output) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="error" label="错误信息" width="200">
|
||||
<template #default="scope">
|
||||
<span class="error-text">{{ scope.row.error || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 性能对比图表 -->
|
||||
<div v-if="comparisonResult.results && comparisonResult.results.length > 0" class="performance-chart">
|
||||
<h3>性能对比</h3>
|
||||
<div ref="chartRef" style="width: 100%; height: 400px;"></div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 报告展示区域 -->
|
||||
<el-card v-if="comparisonReport" class="comparison-report-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>比较报告</span>
|
||||
<el-button @click="downloadReport">
|
||||
<el-icon><download /></el-icon>
|
||||
下载报告
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="report-content">
|
||||
<div class="report-header">
|
||||
<h2>算法效果比较报告</h2>
|
||||
<p>生成时间:{{ formatDate(comparisonReport.report_time) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="report-section">
|
||||
<h3>执行摘要</h3>
|
||||
<p>{{ comparisonReport.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div class="report-section">
|
||||
<h3>性能分析</h3>
|
||||
<div v-html="comparisonReport.performance_analysis"></div>
|
||||
</div>
|
||||
|
||||
<div class="report-section">
|
||||
<h3>建议</h3>
|
||||
<div v-html="comparisonReport.recommendations"></div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { Plus, Loading, Document, Download } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as echarts from 'echarts'
|
||||
import { comparisonService, type ComparisonResult, type ComparisonReport, type AlgorithmConfig } from '../../services/admin'
|
||||
|
||||
// 状态管理
|
||||
const comparing = ref(false)
|
||||
const comparisonResult = ref<any>(null)
|
||||
const comparisonReport = ref<any>(null)
|
||||
const chartRef = ref<HTMLElement>()
|
||||
let chartInstance: echarts.ECharts | null = null
|
||||
|
||||
// 比较表单
|
||||
const comparisonForm = ref({
|
||||
input_data: '{"text": "这是一段测试文本"}',
|
||||
algorithm_configs: <AlgorithmConfig[]>[
|
||||
{
|
||||
algorithm_id: '',
|
||||
algorithm_name: '算法1',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
},
|
||||
{
|
||||
algorithm_id: '',
|
||||
algorithm_name: '算法2',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 添加算法配置
|
||||
const addAlgorithmConfig = () => {
|
||||
comparisonForm.value.algorithm_configs.push({
|
||||
algorithm_id: '',
|
||||
algorithm_name: `算法${comparisonForm.value.algorithm_configs.length + 1}`,
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
})
|
||||
}
|
||||
|
||||
// 删除算法配置
|
||||
const removeAlgorithmConfig = (index: number) => {
|
||||
if (comparisonForm.value.algorithm_configs.length > 2) {
|
||||
comparisonForm.value.algorithm_configs.splice(index, 1)
|
||||
} else {
|
||||
ElMessage.warning('至少需要保留两个算法进行比较')
|
||||
}
|
||||
}
|
||||
|
||||
// 比较算法
|
||||
const compareAlgorithms = async () => {
|
||||
try {
|
||||
// 验证输入数据
|
||||
let inputData
|
||||
try {
|
||||
inputData = JSON.parse(comparisonForm.value.input_data)
|
||||
} catch (error) {
|
||||
ElMessage.error('输入数据格式错误,请输入有效的JSON格式')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证算法配置
|
||||
const validConfigs = comparisonForm.value.algorithm_configs.filter(config => {
|
||||
return config.algorithm_id && config.algorithm_name
|
||||
})
|
||||
|
||||
if (validConfigs.length < 2) {
|
||||
ElMessage.error('至少需要配置两个有效的算法进行比较')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证配置参数
|
||||
validConfigs.forEach(config => {
|
||||
try {
|
||||
if (config.config) {
|
||||
JSON.parse(config.config)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(`算法 ${config.algorithm_name} 的配置参数格式错误`)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
comparing.value = true
|
||||
comparisonResult.value = null
|
||||
comparisonReport.value = null
|
||||
|
||||
comparisonResult.value = await comparisonService.compareAlgorithms(inputData, validConfigs)
|
||||
|
||||
if (comparisonResult.value.success) {
|
||||
ElMessage.success('算法比较完成')
|
||||
// 渲染图表
|
||||
nextTick(() => {
|
||||
renderChart()
|
||||
})
|
||||
} else {
|
||||
ElMessage.error('算法比较失败:' + (comparisonResult.value.error || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('算法比较失败:', error)
|
||||
ElMessage.error('算法比较失败')
|
||||
} finally {
|
||||
comparing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成报告
|
||||
const generateReport = async () => {
|
||||
try {
|
||||
comparisonReport.value = await comparisonService.generateComparisonReport(comparisonResult.value)
|
||||
|
||||
if (comparisonReport.value.success) {
|
||||
ElMessage.success('报告生成成功')
|
||||
} else {
|
||||
ElMessage.error('报告生成失败:' + (comparisonReport.value.error || '未知错误'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成报告失败:', error)
|
||||
ElMessage.error('生成报告失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
const renderChart = () => {
|
||||
if (!chartRef.value || !comparisonResult.value.results) return
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
|
||||
const results = comparisonResult.value.results
|
||||
const algorithmNames = results.map((r: any) => r.algorithm_name)
|
||||
const executionTimes = results.map((r: any) => r.execution_time || 0)
|
||||
const successRates = results.map((r: any) => r.success ? 100 : 0)
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: '算法性能对比'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['执行时间(ms)', '成功率(%)']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: algorithmNames
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '执行时间(ms)',
|
||||
position: 'left'
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '成功率(%)',
|
||||
position: 'right',
|
||||
max: 100
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '执行时间(ms)',
|
||||
type: 'bar',
|
||||
data: executionTimes,
|
||||
yAxisIndex: 0
|
||||
},
|
||||
{
|
||||
name: '成功率(%)',
|
||||
type: 'line',
|
||||
data: successRates,
|
||||
yAxisIndex: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 下载报告
|
||||
const downloadReport = () => {
|
||||
if (!comparisonReport.value) return
|
||||
|
||||
const reportContent = `
|
||||
算法效果比较报告
|
||||
生成时间:${formatDate(comparisonReport.value.report_time)}
|
||||
|
||||
执行摘要:
|
||||
${comparisonReport.value.summary}
|
||||
|
||||
性能分析:
|
||||
${comparisonReport.value.performance_analysis}
|
||||
|
||||
建议:
|
||||
${comparisonReport.value.recommendations}
|
||||
`
|
||||
|
||||
const blob = new Blob([reportContent], { type: 'text/plain;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `算法比较报告_${new Date().getTime()}.txt`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('报告下载成功')
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
comparisonForm.value = {
|
||||
input_data: '{"text": "这是一段测试文本"}',
|
||||
algorithm_configs: <AlgorithmConfig[]>[
|
||||
{
|
||||
algorithm_id: '',
|
||||
algorithm_name: '算法1',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
},
|
||||
{
|
||||
algorithm_id: '',
|
||||
algorithm_name: '算法2',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
}
|
||||
]
|
||||
}
|
||||
comparisonResult.value = null
|
||||
comparisonReport.value = null
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 格式化输出
|
||||
const formatOutput = (output: any) => {
|
||||
if (typeof output === 'object') {
|
||||
return JSON.stringify(output).substring(0, 100) + (JSON.stringify(output).length > 100 ? '...' : '')
|
||||
}
|
||||
return String(output).substring(0, 100) + (String(output).length > 100 ? '...' : '')
|
||||
}
|
||||
|
||||
// 响应式图表
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.algorithm-comparison-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.algorithm-comparison-container h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comparison-config-card,
|
||||
.comparison-result-card,
|
||||
.comparison-report-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.algorithm-configs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.algorithm-config-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-algorithm-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-overview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detailed-results {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detailed-results h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.output-cell {
|
||||
max-height: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.performance-chart {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.performance-chart h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.report-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.report-header h2 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.report-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.report-section h3 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 2px solid #409eff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.algorithm-configs {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,9 +13,9 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
注册新服务
|
||||
</el-button>
|
||||
<el-button type="info" @click="showServiceDetailDialog = false">
|
||||
<el-icon><View /></el-icon>
|
||||
查看服务详情
|
||||
<el-button type="info" @click="showHelpDialog = true">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
帮助说明
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -66,9 +66,11 @@
|
||||
<el-table-column prop="host" label="主机" width="150" />
|
||||
<el-table-column prop="port" label="端口" width="80" />
|
||||
<el-table-column prop="api_url" label="API地址" />
|
||||
<el-table-column prop="last_heartbeat" label="最后心跳" width="180">
|
||||
<el-table-column prop="health_check" label="健康检查" width="200">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.last_heartbeat) }}
|
||||
<el-link :href="scope.row.api_url + '/health'" target="_blank" type="primary">
|
||||
{{ scope.row.api_url }}/health
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
@@ -97,8 +99,17 @@
|
||||
<el-descriptions-item label="主机">{{ selectedService.host }}</el-descriptions-item>
|
||||
<el-descriptions-item label="端口">{{ selectedService.port }}</el-descriptions-item>
|
||||
<el-descriptions-item label="API地址">{{ selectedService.api_url }}</el-descriptions-item>
|
||||
<el-descriptions-item label="健康检查">
|
||||
<el-link :href="selectedService.api_url + '/health'" target="_blank" type="primary">
|
||||
{{ selectedService.api_url }}/health
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务信息">
|
||||
<el-link :href="selectedService.api_url + '/info'" target="_blank" type="primary">
|
||||
{{ selectedService.api_url }}/info
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="启动时间">{{ formatDate(selectedService.start_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后心跳">{{ formatDate(selectedService.last_heartbeat) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务描述" :span="2">{{ selectedService.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
@@ -131,14 +142,169 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 帮助说明对话框 -->
|
||||
<el-dialog
|
||||
v-model="showHelpDialog"
|
||||
title="算法服务管理说明"
|
||||
width="70%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="help-dialog-content">
|
||||
<el-collapse v-model="activeHelpSections">
|
||||
<el-collapse-item title="什么是算法服务管理?" name="what">
|
||||
<div class="help-section">
|
||||
<p><strong>算法服务管理</strong>是智能算法展示平台的核心功能,用于管理从算法仓库部署的算法服务。</p>
|
||||
<p>它将您的算法代码转换为可调用的API服务,让算法能够通过HTTP接口被其他应用调用。</p>
|
||||
<el-divider />
|
||||
<h4>主要功能:</h4>
|
||||
<ul>
|
||||
<li><strong>服务注册</strong>:将算法仓库中的算法部署为独立的服务</li>
|
||||
<li><strong>服务监控</strong>:实时监控服务的运行状态和健康情况</li>
|
||||
<li><strong>服务控制</strong>:启动、停止、重启已部署的服务</li>
|
||||
<li><strong>服务调用</strong>:通过统一接口调用算法服务</li>
|
||||
<li><strong>日志查看</strong>:查看服务运行日志,便于问题排查</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="为什么需要服务管理?" name="why">
|
||||
<div class="help-section">
|
||||
<h4>解决的问题:</h4>
|
||||
<ul>
|
||||
<li><strong>算法隔离</strong>:每个算法运行在独立的环境中,避免相互干扰</li>
|
||||
<li><strong>资源管理</strong>:统一管理CPU、内存等资源分配</li>
|
||||
<li><strong>高可用性</strong>:支持服务的自动重启和故障恢复</li>
|
||||
<li><strong>负载均衡</strong>:支持多实例部署,提高并发处理能力</li>
|
||||
<li><strong>版本管理</strong>:同时运行不同版本的算法,便于A/B测试</li>
|
||||
</ul>
|
||||
<el-divider />
|
||||
<h4>应用场景:</h4>
|
||||
<ul>
|
||||
<li><strong>算法展示</strong>:为客户演示算法效果和能力</li>
|
||||
<li><strong>API服务</strong>:为其他应用提供算法调用接口</li>
|
||||
<li><strong>性能测试</strong>:对比不同算法的效果和性能</li>
|
||||
<li><strong>生产部署</strong>:将算法部署到生产环境提供服务</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何使用服务管理?" name="how">
|
||||
<div class="help-section">
|
||||
<h4>使用流程:</h4>
|
||||
<el-steps :active="currentStep" finish-status="success" align-center>
|
||||
<el-step title="上传算法代码" description="将算法代码上传到Gitea仓库" />
|
||||
<el-step title="注册服务" description="从仓库选择算法并注册为服务" />
|
||||
<el-step title="部署服务" description="系统自动部署服务并启动" />
|
||||
<el-step title="调用服务" description="通过API或前端页面调用算法" />
|
||||
</el-steps>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>详细步骤:</h4>
|
||||
<ol>
|
||||
<li><strong>上传算法代码</strong>:在"算法仓库管理"中上传您的算法项目代码</li>
|
||||
<li><strong>注册服务</strong>:点击"注册新服务"按钮,选择要部署的算法</li>
|
||||
<li><strong>配置服务</strong>:设置服务名称、端口、环境变量等配置</li>
|
||||
<li><strong>启动服务</strong>:系统会自动部署并启动服务</li>
|
||||
<li><strong>测试调用</strong>:使用服务调用功能测试算法是否正常工作</li>
|
||||
<li><strong>监控服务</strong>:查看服务状态、日志和性能指标</li>
|
||||
</ol>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="服务状态说明" name="status">
|
||||
<div class="help-section">
|
||||
<h4>服务状态类型:</h4>
|
||||
<el-table :data="statusDescriptions" style="width: 100%">
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" />
|
||||
<el-table-column prop="action" label="建议操作" width="150" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="常见问题" name="faq">
|
||||
<div class="help-section">
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="服务无法启动怎么办?">
|
||||
<p>检查以下几点:</p>
|
||||
<ul>
|
||||
<li>算法代码是否有语法错误</li>
|
||||
<li>依赖包是否正确安装</li>
|
||||
<li>端口是否被占用</li>
|
||||
<li>查看服务日志获取详细错误信息</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何调用已部署的服务?">
|
||||
<p>有三种调用方式:</p>
|
||||
<ul>
|
||||
<li><strong>前端调用</strong>:在算法调用页面选择服务进行调用</li>
|
||||
<li><strong>API调用</strong>:使用POST /api/v1/services/call接口调用</li>
|
||||
<li><strong>网关调用</strong>:通过API网关统一调用所有算法</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何验证服务是否正常运行?">
|
||||
<p>可以通过以下方式验证服务状态:</p>
|
||||
<ul>
|
||||
<li><strong>健康检查</strong>:访问服务的 /health 端点</li>
|
||||
<li><strong>服务信息</strong>:访问服务的 /info 端点</li>
|
||||
<li><strong>直接访问</strong>:在浏览器中访问服务的API地址</li>
|
||||
</ul>
|
||||
<p>例如,如果服务的API地址是 http://0.0.0.0:8005,可以访问:</p>
|
||||
<ul>
|
||||
<li>http://0.0.0.0:8005/health - 健康检查端点</li>
|
||||
<li>http://0.0.0.0:8005/info - 服务信息端点</li>
|
||||
</ul>
|
||||
<p>正常情况下,/health 端点会返回 {"status": "healthy", "service": "服务名"}</p>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="服务性能如何优化?">
|
||||
<p>优化建议:</p>
|
||||
<ul>
|
||||
<li>调整服务配置中的超时时间</li>
|
||||
<li>增加服务的并发处理能力</li>
|
||||
<li>使用更高效的算法实现</li>
|
||||
<li>考虑使用GPU加速</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何查看服务日志?">
|
||||
<p>点击服务列表中的"详情"按钮,在弹出的对话框中可以查看:</p>
|
||||
<ul>
|
||||
<li>服务基本信息</li>
|
||||
<li>服务配置详情</li>
|
||||
<li>实时运行日志</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showHelpDialog = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Refresh, View, Plus } from '@element-plus/icons-vue'
|
||||
import { Refresh, View, Plus, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { serviceManagementApi } from '../../services/serviceManagement'
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
@@ -146,8 +312,42 @@ const router = useRouter()
|
||||
// 状态管理
|
||||
const services = ref<any[]>([])
|
||||
const showServiceDetailDialog = ref(false)
|
||||
const showHelpDialog = ref(false)
|
||||
const selectedService = ref<any>(null)
|
||||
|
||||
// 帮助说明相关状态
|
||||
const activeHelpSections = ref<string[]>(['what'])
|
||||
const currentStep = ref(0)
|
||||
|
||||
// 服务状态说明数据
|
||||
const statusDescriptions = ref([
|
||||
{
|
||||
status: 'running',
|
||||
description: '服务正在正常运行,可以接收和处理请求',
|
||||
action: '可以调用服务'
|
||||
},
|
||||
{
|
||||
status: 'stopped',
|
||||
description: '服务已停止,无法接收请求',
|
||||
action: '点击启动按钮启动服务'
|
||||
},
|
||||
{
|
||||
status: 'error',
|
||||
description: '服务运行出错,需要检查日志排查问题',
|
||||
action: '查看日志并修复问题后重启'
|
||||
},
|
||||
{
|
||||
status: 'starting',
|
||||
description: '服务正在启动中,请稍候',
|
||||
action: '等待启动完成'
|
||||
},
|
||||
{
|
||||
status: 'stopping',
|
||||
description: '服务正在停止中,请稍候',
|
||||
action: '等待停止完成'
|
||||
}
|
||||
])
|
||||
|
||||
// 计算服务统计信息
|
||||
const totalServices = computed(() => services.value.length)
|
||||
const runningServices = computed(() => services.value.filter(s => s.status === 'running').length)
|
||||
@@ -178,30 +378,10 @@ const formatDate = (dateString: string) => {
|
||||
// 加载服务列表
|
||||
const loadServices = async () => {
|
||||
try {
|
||||
// 从本地存储获取token
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
ElMessage.error('未登录,请重新登录')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用后端API获取服务列表
|
||||
const response = await fetch('http://0.0.0.0:8001/api/v1/services', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('获取服务列表失败')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
const result = await serviceManagementApi.getServices()
|
||||
if (result.success) {
|
||||
// 处理服务数据,添加缺失的字段
|
||||
services.value = data.services.map((service: any) => ({
|
||||
services.value = result.services.map((service: any) => ({
|
||||
...service,
|
||||
last_heartbeat: service.last_heartbeat || null,
|
||||
start_time: service.start_time || null,
|
||||
@@ -211,7 +391,7 @@ const loadServices = async () => {
|
||||
}))
|
||||
console.log('服务列表加载完成', services.value)
|
||||
} else {
|
||||
throw new Error(data.message || '获取服务列表失败')
|
||||
throw new Error(result.message || '获取服务列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载服务列表失败:', error)
|
||||
@@ -229,14 +409,15 @@ const refreshServices = async () => {
|
||||
const startService = async (service: any) => {
|
||||
try {
|
||||
console.log('启动服务:', service.name)
|
||||
// 这里应该调用后端API启动服务
|
||||
|
||||
// 模拟启动服务
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
const result = await serviceManagementApi.startService(service.service_id)
|
||||
|
||||
// 更新服务状态
|
||||
service.status = 'running'
|
||||
ElMessage.success('服务启动成功')
|
||||
if (result.success) {
|
||||
ElMessage.success('服务启动成功')
|
||||
await refreshServices()
|
||||
} else {
|
||||
ElMessage.error(`服务启动失败: ${result.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动服务失败:', error)
|
||||
ElMessage.error('启动服务失败')
|
||||
@@ -253,16 +434,20 @@ const stopService = async (service: any) => {
|
||||
})
|
||||
|
||||
console.log('停止服务:', service.name)
|
||||
// 这里应该调用后端API停止服务
|
||||
|
||||
// 模拟停止服务
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
const result = await serviceManagementApi.stopService(service.service_id)
|
||||
|
||||
// 更新服务状态
|
||||
service.status = 'stopped'
|
||||
ElMessage.success('服务停止成功')
|
||||
if (result.success) {
|
||||
ElMessage.success('服务停止成功')
|
||||
await refreshServices()
|
||||
} else {
|
||||
ElMessage.error(`服务停止失败: ${result.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
if (error !== 'cancel') {
|
||||
console.error('停止服务失败:', error)
|
||||
ElMessage.error('停止服务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,14 +455,15 @@ const stopService = async (service: any) => {
|
||||
const restartService = async (service: any) => {
|
||||
try {
|
||||
console.log('重启服务:', service.name)
|
||||
// 这里应该调用后端API重启服务
|
||||
|
||||
// 模拟重启服务
|
||||
service.status = 'restarting'
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
service.status = 'running'
|
||||
const result = await serviceManagementApi.restartService(service.service_id)
|
||||
|
||||
ElMessage.success('服务重启成功')
|
||||
if (result.success) {
|
||||
ElMessage.success('服务重启成功')
|
||||
await refreshServices()
|
||||
} else {
|
||||
ElMessage.error(`服务重启失败: ${result.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重启服务失败:', error)
|
||||
ElMessage.error('重启服务失败')
|
||||
@@ -315,6 +501,45 @@ onMounted(async () => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.help-section h4 {
|
||||
margin: 15px 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.help-section p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section ul,
|
||||
.help-section ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-section li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section strong {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-dialog-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.status-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
|
||||
@@ -240,6 +240,7 @@ import { ref, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Plus, Setting, ArrowDown, UploadFilled, Edit, Folder } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
|
||||
// 获取路由
|
||||
const router = useRouter()
|
||||
@@ -465,8 +466,6 @@ const uploadFilesToServer = async (files: File[], algorithmId?: string) => {
|
||||
return await uploadFilesInBatches(files, algorithmId, MAX_BATCH_SIZE);
|
||||
}
|
||||
|
||||
const axios = await import('axios')
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
// 添加所有文件到 FormData
|
||||
@@ -493,9 +492,11 @@ const uploadFilesToServer = async (files: File[], algorithmId?: string) => {
|
||||
console.log('=== 发送上传请求 ===')
|
||||
console.log('Sending request to /api/gitea/repos/upload')
|
||||
|
||||
const response = await axios.default.post('/api/gitea/repos/upload', formData, {
|
||||
const token = localStorage.getItem('token') || ''
|
||||
const response = await axios.post('/api/gitea/repos/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
// 监控上传进度
|
||||
onUploadProgress: (progressEvent) => {
|
||||
@@ -540,70 +541,74 @@ const uploadFilesToServer = async (files: File[], algorithmId?: string) => {
|
||||
|
||||
// 分批上传文件
|
||||
const uploadFilesInBatches = async (files: File[], algorithmId?: string, limit: number = 50 * 1024 * 1024) => {
|
||||
console.log('开始分批上传文件...');
|
||||
try {
|
||||
console.log('开始分批上传文件...');
|
||||
|
||||
// 判断是基于文件数量还是文件大小分批
|
||||
const MAX_FILES_PER_BATCH = 1000 // Chrome 浏览器限制为 1000 个文件
|
||||
const isFileCountBatch = files.length > MAX_FILES_PER_BATCH
|
||||
// 判断是基于文件数量还是文件大小分批
|
||||
const MAX_FILES_PER_BATCH = 1000 // Chrome 浏览器限制为 1000 个文件
|
||||
const isFileCountBatch = files.length > MAX_FILES_PER_BATCH
|
||||
|
||||
// 按批次分割文件
|
||||
const batches: File[][] = [];
|
||||
let currentBatch: File[] = [];
|
||||
let currentBatchSize = 0;
|
||||
// 按批次分割文件
|
||||
const batches: File[][] = [];
|
||||
let currentBatch: File[] = [];
|
||||
let currentBatchSize = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (isFileCountBatch) {
|
||||
// 基于文件数量分批 (Chrome 限制 1000)
|
||||
if (currentBatch.length >= MAX_FILES_PER_BATCH) {
|
||||
batches.push([...currentBatch]);
|
||||
currentBatch = [file];
|
||||
for (const file of files) {
|
||||
if (isFileCountBatch) {
|
||||
// 基于文件数量分批 (Chrome 限制 1000)
|
||||
if (currentBatch.length >= MAX_FILES_PER_BATCH) {
|
||||
batches.push([...currentBatch]);
|
||||
currentBatch = [file];
|
||||
} else {
|
||||
currentBatch.push(file);
|
||||
}
|
||||
} else {
|
||||
currentBatch.push(file);
|
||||
}
|
||||
} else {
|
||||
// 基于文件大小分批
|
||||
if (currentBatchSize + file.size > limit && currentBatch.length > 0) {
|
||||
batches.push([...currentBatch]);
|
||||
currentBatch = [file];
|
||||
currentBatchSize = file.size;
|
||||
} else {
|
||||
currentBatch.push(file);
|
||||
currentBatchSize += file.size;
|
||||
// 基于文件大小分批
|
||||
if (currentBatchSize + file.size > limit && currentBatch.length > 0) {
|
||||
batches.push([...currentBatch]);
|
||||
currentBatch = [file];
|
||||
currentBatchSize = file.size;
|
||||
} else {
|
||||
currentBatch.push(file);
|
||||
currentBatchSize += file.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加最后一个批次
|
||||
if (currentBatch.length > 0) {
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
|
||||
console.log(`总共 ${files.length} 个文件分为 ${batches.length} 批`);
|
||||
console.log(`分批模式: ${isFileCountBatch ? '基于文件数量' : '基于文件大小'}`);
|
||||
|
||||
// 逐批上传
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
console.log(`上传第 ${i + 1} 批文件,包含 ${batches[i].length} 个文件`);
|
||||
|
||||
const batchResult = await uploadSingleBatch(batches[i], algorithmId);
|
||||
if (!batchResult) {
|
||||
console.error(`第 ${i + 1} 批上传失败`);
|
||||
return false;
|
||||
// 添加最后一个批次
|
||||
if (currentBatch.length > 0) {
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
const progressPerBatch = Math.floor(100 / batches.length);
|
||||
uploadProgress.value = Math.min(progressPerBatch * (i + 1), 90); // 最多到90%,保留10%给最后的推送
|
||||
}
|
||||
console.log(`总共 ${files.length} 个文件分为 ${batches.length} 批`);
|
||||
console.log(`分批模式: ${isFileCountBatch ? '基于文件数量' : '基于文件大小'}`);
|
||||
|
||||
console.log('所有批次上传完成');
|
||||
return true;
|
||||
// 逐批上传
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
console.log(`上传第 ${i + 1} 批文件,包含 ${batches[i].length} 个文件`);
|
||||
|
||||
const batchResult = await uploadSingleBatch(batches[i], algorithmId);
|
||||
if (!batchResult) {
|
||||
console.error(`第 ${i + 1} 批上传失败`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
const progressPerBatch = Math.floor(100 / batches.length);
|
||||
uploadProgress.value = Math.min(progressPerBatch * (i + 1), 90); // 最多到90%,保留10%给最后的推送
|
||||
}
|
||||
|
||||
console.log('所有批次上传完成');
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error('分批上传失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 上传单个批次
|
||||
const uploadSingleBatch = async (batch: File[], algorithmId?: string) => {
|
||||
try {
|
||||
const axios = await import('axios');
|
||||
const formData = new FormData();
|
||||
|
||||
for (let i = 0; i < batch.length; i++) {
|
||||
@@ -614,12 +619,14 @@ const uploadSingleBatch = async (batch: File[], algorithmId?: string) => {
|
||||
|
||||
const finalAlgorithmId = algorithmId || repoForm.value.gitRepoName;
|
||||
formData.append('algorithm_id', finalAlgorithmId);
|
||||
|
||||
|
||||
console.log(`上传批次,包含 ${batch.length} 个文件`);
|
||||
|
||||
const response = await axios.default.post('/api/gitea/repos/upload', formData, {
|
||||
|
||||
const token = localStorage.getItem('token') || ''
|
||||
const response = await axios.post('/api/gitea/repos/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: 600000 // 10分钟超时
|
||||
});
|
||||
@@ -732,24 +739,40 @@ const formatDate = (dateString: string) => {
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 获取带认证的axios实例
|
||||
const getAxiosWithAuth = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return {
|
||||
axios: axios,
|
||||
token: token || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 加载仓库列表
|
||||
const loadRepos = async () => {
|
||||
// 检查用户是否已登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
console.log('用户未登录,跳过加载仓库列表')
|
||||
ElMessage.warning('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
|
||||
console.log('开始加载仓库列表...')
|
||||
|
||||
// 调用后端API获取仓库列表
|
||||
const response = await axios.default.get('/api/repositories')
|
||||
|
||||
console.log('Token:', token ? `${token.substring(0, 20)}...` : 'none')
|
||||
console.log('Token长度:', token?.length)
|
||||
|
||||
// 调用后端API获取仓库列表,认证头会自动添加
|
||||
const response = await axios.get('/api/repositories')
|
||||
|
||||
console.log('仓库列表API响应:', response)
|
||||
console.log('响应状态:', response.status)
|
||||
console.log('响应数据:', response.data)
|
||||
console.log('响应数据类型:', typeof response.data)
|
||||
console.log('success字段:', response.data.success)
|
||||
console.log('repositories字段:', response.data.repositories)
|
||||
|
||||
|
||||
if (response.data.success) {
|
||||
repos.value = response.data.repositories
|
||||
console.log('仓库列表加载完成,共', repos.value.length, '个仓库')
|
||||
@@ -758,11 +781,20 @@ const loadRepos = async () => {
|
||||
console.error('加载仓库列表失败:success为false')
|
||||
ElMessage.error('加载仓库列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('加载仓库列表失败:', error)
|
||||
console.error('错误类型:', typeof error)
|
||||
console.error('错误详情:', error)
|
||||
ElMessage.error('加载仓库列表失败')
|
||||
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
console.error('认证失败,请重新登录')
|
||||
ElMessage.error('认证失败,请重新登录')
|
||||
// 清除无效的token
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
} else {
|
||||
ElMessage.error('加载仓库列表失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,46 +810,58 @@ const addRepo = async () => {
|
||||
ElMessage.error('请填写完整的仓库信息')
|
||||
return
|
||||
}
|
||||
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
|
||||
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
// 调用后端API添加仓库
|
||||
const response = await axios.default.post('/api/repositories', {
|
||||
const response = await axios.post('/api/repositories', {
|
||||
name: repoForm.value.name,
|
||||
description: repoForm.value.description,
|
||||
type: repoForm.value.type,
|
||||
repo_url: repoForm.value.repo_url,
|
||||
branch: repoForm.value.branch,
|
||||
local_path: repoForm.value.local_path
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (response.data.success) {
|
||||
// 显示上传进度对话框
|
||||
isUploading.value = true
|
||||
uploadProgress.value = 0
|
||||
|
||||
|
||||
try {
|
||||
// 计算包含前缀的完整仓库名称
|
||||
// 前端根据配置添加前缀,后端直接使用
|
||||
const repoPrefix = giteaConfigForm.value.repoPrefix || ''
|
||||
const fullRepoName = repoPrefix + repoForm.value.gitRepoName
|
||||
|
||||
|
||||
// 首先在Gitea上创建仓库
|
||||
const createResponse = await axios.default.post('/api/gitea/repos/create', {
|
||||
const createResponse = await axios.post('/api/gitea/repos/create', {
|
||||
algorithm_id: fullRepoName,
|
||||
algorithm_name: repoForm.value.name,
|
||||
description: repoForm.value.description
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
console.log('仓库创建响应:', createResponse.data)
|
||||
|
||||
|
||||
// 然后克隆仓库(如果需要)
|
||||
try {
|
||||
const cloneResponse = await axios.default.post('/api/gitea/repos/clone', {
|
||||
const cloneResponse = await axios.post('/api/gitea/repos/clone', {
|
||||
repo_url: repoForm.value.repo_url,
|
||||
algorithm_id: fullRepoName,
|
||||
branch: repoForm.value.branch
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
console.log('仓库克隆响应:', cloneResponse.data)
|
||||
} catch (cloneError: any) {
|
||||
@@ -839,11 +883,15 @@ const addRepo = async () => {
|
||||
}
|
||||
|
||||
// 最后推送代码到Gitea仓库
|
||||
const pushResponse = await axios.default.post('/api/gitea/repos/push', {
|
||||
const pushResponse = await axios.post('/api/gitea/repos/push', {
|
||||
algorithm_id: fullRepoName,
|
||||
message: `Initial commit for ${repoForm.value.name}`
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (pushResponse.data.success) {
|
||||
ElMessage.success('仓库添加成功,代码已上传到Gitea')
|
||||
} else {
|
||||
@@ -892,13 +940,18 @@ const addRepo = async () => {
|
||||
// 编辑仓库
|
||||
const editRepo = async (repo: any) => {
|
||||
console.log('编辑仓库:', repo)
|
||||
|
||||
|
||||
try {
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
// 调用后端API获取仓库详细信息
|
||||
const response = await axios.default.get(`/api/repositories/${repo.id}`)
|
||||
const response = await axios.get(`/api/repositories/${repo.id}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
const repoDetails = response.data.repository
|
||||
@@ -943,14 +996,13 @@ const extractRepoName = (repoUrl: string) => {
|
||||
const updateRepo = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
if (!editRepoForm.value.name || !editRepoForm.value.description ||
|
||||
if (!editRepoForm.value.name || !editRepoForm.value.description ||
|
||||
!editRepoForm.value.type || !editRepoForm.value.gitRepoName) {
|
||||
ElMessage.error('请填写完整的仓库信息')
|
||||
return
|
||||
}
|
||||
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
console.log('正在更新仓库:', {
|
||||
repoId: currentEditingRepo.value.id,
|
||||
@@ -960,10 +1012,14 @@ const updateRepo = async () => {
|
||||
})
|
||||
|
||||
// 调用后端API更新仓库 - 只更新名称、描述和类型
|
||||
const response = await axios.default.put(`/api/repositories/${currentEditingRepo.value.id}`, {
|
||||
const response = await axios.put(`/api/repositories/${currentEditingRepo.value.id}`, {
|
||||
name: editRepoForm.value.name,
|
||||
description: editRepoForm.value.description,
|
||||
type: editRepoForm.value.type
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
@@ -976,7 +1032,7 @@ const updateRepo = async () => {
|
||||
console.log('更新Gitea仓库信息,使用algorithm_id:', algorithmId)
|
||||
|
||||
// 调用 Gitea API 更新仓库信息 - 只更新描述,不更新名称
|
||||
const giteaResponse = await axios.default.patch('/api/gitea/repos/update', {
|
||||
const giteaResponse = await axios.patch('/api/gitea/repos/update', {
|
||||
algorithm_id: algorithmId,
|
||||
description: editRepoForm.value.description,
|
||||
private: false // 暂时默认为公开仓库,后续可以添加专门的私有仓库选项
|
||||
@@ -996,7 +1052,7 @@ const updateRepo = async () => {
|
||||
} else {
|
||||
console.log('Gitea仓库信息更新失败')
|
||||
}
|
||||
} catch (giteaError) {
|
||||
} catch (giteaError: any) {
|
||||
console.error('更新Gitea仓库信息失败:', giteaError)
|
||||
console.error('错误详情:', giteaError.response?.data || giteaError.message)
|
||||
// Gitea更新失败不影响本地更新的成功状态
|
||||
@@ -1038,13 +1094,18 @@ const deleteRepo = async (repoId: string) => {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
// 调用后端API删除仓库
|
||||
const response = await axios.default.delete(`/api/repositories/${repoId}`)
|
||||
|
||||
const response = await axios.delete(`/api/repositories/${repoId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('仓库删除成功')
|
||||
await loadRepos()
|
||||
@@ -1093,16 +1154,21 @@ const saveGiteaConfig = async () => {
|
||||
ElMessage.error('请填写完整的Gitea配置信息')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
// 调用后端API保存配置
|
||||
const response = await axios.default.post('/api/gitea/config', {
|
||||
const response = await axios.post('/api/gitea/config', {
|
||||
server_url: giteaConfigForm.value.serverUrl,
|
||||
access_token: giteaConfigForm.value.accessToken,
|
||||
default_owner: giteaConfigForm.value.defaultOwner,
|
||||
repo_prefix: giteaConfigForm.value.repoPrefix || ''
|
||||
}, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
@@ -1159,12 +1225,23 @@ const cancelEditGiteaConfig = () => {
|
||||
|
||||
// 加载已保存的Gitea配置
|
||||
const loadGiteaConfig = async () => {
|
||||
// 检查用户是否已登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
console.log('用户未登录,跳过加载Gitea配置')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 导入axios
|
||||
const axios = await import('axios')
|
||||
|
||||
const response = await axios.default.get('/api/gitea/config')
|
||||
|
||||
const { default: axios } = await import('axios')
|
||||
|
||||
const response = await axios.get('/api/gitea/config', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data) {
|
||||
const config = response.data
|
||||
giteaConfigForm.value = {
|
||||
@@ -1174,9 +1251,13 @@ const loadGiteaConfig = async () => {
|
||||
repoPrefix: config.repo_prefix || ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载Gitea配置失败:', error)
|
||||
// 配置不存在时不显示错误
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 401 || error.response?.status === 404) {
|
||||
// 配置不存在或未认证,不显示错误
|
||||
console.log('Gitea配置不存在或需要认证')
|
||||
} else {
|
||||
console.error('加载Gitea配置失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,54 @@
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="showAddDialog = true">
|
||||
<el-icon><plus /></el-icon>
|
||||
<el-icon><Plus /></el-icon>
|
||||
封装新API
|
||||
</el-button>
|
||||
<el-button @click="loadApis">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button type="info" @click="showHelpDialog = true">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
帮助说明
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- API统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ apiStats.total_endpoints }}</div>
|
||||
<div class="stat-label">总API数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ apiStats.active_endpoints }}</div>
|
||||
<div class="stat-label">激活API</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ apiStats.total_calls }}</div>
|
||||
<div class="stat-label">总调用次数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ apiStats.avg_response_time }}s</div>
|
||||
<div class="stat-label">平均响应时间</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="filterStatus" placeholder="API状态" clearable @change="loadApis">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="激活" value="active" />
|
||||
<el-option label="停用" value="inactive" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- API列表 -->
|
||||
@@ -16,6 +61,7 @@
|
||||
<el-table-column prop="name" label="API名称" width="200" />
|
||||
<el-table-column prop="algorithm_name" label="算法名称" width="180" />
|
||||
<el-table-column prop="version" label="版本" width="120" />
|
||||
<el-table-column prop="path" label="API路径" width="200" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
@@ -23,13 +69,16 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="call_count" label="调用次数" width="100" />
|
||||
<el-table-column prop="avg_response_time" label="平均响应时间" width="120" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="testApi(scope.row)">测试</el-button>
|
||||
<el-button size="small" @click="editApi(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteApi(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
@@ -39,15 +88,26 @@
|
||||
<!-- 封装API对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
title="封装新API"
|
||||
:title="isEditMode ? '编辑API' : '封装新API'"
|
||||
width="70%"
|
||||
>
|
||||
<el-form :model="apiForm" :rules="apiRules" ref="apiFormRef">
|
||||
<el-form-item label="API名称" prop="name">
|
||||
<el-input v-model="apiForm.name" placeholder="请输入API名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API路径" prop="path">
|
||||
<el-input v-model="apiForm.path" placeholder="请输入API路径,例如:/api/image-classification" />
|
||||
</el-form-item>
|
||||
<el-form-item label="HTTP方法" prop="method">
|
||||
<el-select v-model="apiForm.method" placeholder="选择HTTP方法">
|
||||
<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-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择算法" prop="algorithm_id">
|
||||
<el-select v-model="apiForm.algorithm_id" placeholder="选择算法">
|
||||
<el-select v-model="apiForm.algorithm_id" placeholder="选择算法" @change="handleAlgorithmChange">
|
||||
<el-option
|
||||
v-for="algorithm in algorithms"
|
||||
:key="algorithm.id"
|
||||
@@ -66,8 +126,11 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="API路径" prop="path">
|
||||
<el-input v-model="apiForm.path" placeholder="请输入API路径,例如:/api/predict" />
|
||||
<el-form-item label="是否公开">
|
||||
<el-switch v-model="apiForm.is_public" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要认证">
|
||||
<el-switch v-model="apiForm.requires_auth" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
@@ -80,8 +143,172 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showAddDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="addApi">保存</el-button>
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveApi">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 测试API对话框 -->
|
||||
<el-dialog
|
||||
v-model="showTestDialog"
|
||||
title="测试API"
|
||||
width="60%"
|
||||
>
|
||||
<el-form :model="testForm" ref="testFormRef">
|
||||
<el-form-item label="API信息">
|
||||
<div class="api-info">
|
||||
<p><strong>API名称:</strong>{{ testingApi?.name }}</p>
|
||||
<p><strong>API路径:</strong>{{ testingApi?.path }}</p>
|
||||
<p><strong>HTTP方法:</strong>{{ testingApi?.method }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求数据">
|
||||
<el-input
|
||||
v-model="testForm.payload"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder='请输入JSON格式的请求数据,例如:{"text":"测试数据","number":123}'
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showTestDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="executeTest" :loading="testing">测试</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 帮助说明对话框 -->
|
||||
<el-dialog
|
||||
v-model="showHelpDialog"
|
||||
title="API管理说明"
|
||||
width="70%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="help-dialog-content">
|
||||
<el-collapse v-model="activeHelpSections">
|
||||
<el-collapse-item title="什么是API管理?" name="what">
|
||||
<div class="help-section">
|
||||
<p><strong>API管理</strong>是智能算法展示平台的核心功能,用于封装和管理算法API端点。</p>
|
||||
<p>它将算法服务封装成统一的API接口,提供标准化的调用方式,支持权限控制、流量限制等功能。</p>
|
||||
<el-divider />
|
||||
<h4>主要功能:</h4>
|
||||
<ul>
|
||||
<li><strong>API封装</strong>:将算法服务封装成标准化的API端点</li>
|
||||
<li><strong>权限控制</strong>:配置API的访问权限和角色限制</li>
|
||||
<li><strong>流量限制</strong>:设置API的调用频率限制</li>
|
||||
<li><strong>API测试</strong>:在线测试API是否正常工作</li>
|
||||
<li><strong>调用统计</strong>:统计API的调用次数和成功率</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="为什么需要API管理?" name="why">
|
||||
<div class="help-section">
|
||||
<h4>解决的问题:</h4>
|
||||
<ul>
|
||||
<li><strong>接口标准化</strong>:统一API调用方式,降低集成复杂度</li>
|
||||
<li><strong>权限管理</strong>:精细控制API的访问权限</li>
|
||||
<li><strong>流量控制</strong>:防止API被滥用,保护服务稳定性</li>
|
||||
<li><strong>监控统计</strong>:实时监控API调用情况</li>
|
||||
<li><strong>文档生成</strong>:自动生成API文档</li>
|
||||
</ul>
|
||||
<el-divider />
|
||||
<h4>应用场景:</h4>
|
||||
<ul>
|
||||
<li><strong>对外开放</strong>:为外部应用提供算法API</li>
|
||||
<li><strong>内部集成</strong>:为其他系统提供算法服务</li>
|
||||
<li><strong>API市场</strong>:发布API到API市场供他人使用</li>
|
||||
<li><strong>微服务架构</strong>:构建基于API的微服务架构</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何使用API管理?" name="how">
|
||||
<div class="help-section">
|
||||
<h4>使用流程:</h4>
|
||||
<el-steps :active="currentStep" finish-status="success" align-center>
|
||||
<el-step title="选择算法" description="选择要封装的算法和版本" />
|
||||
<el-step title="配置API" description="设置API路径、权限等配置" />
|
||||
<el-step title="测试API" description="测试API是否正常工作" />
|
||||
<el-step title="发布API" description="发布API供他人使用" />
|
||||
</el-steps>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>详细步骤:</h4>
|
||||
<ol>
|
||||
<li><strong>选择算法</strong>:从算法列表中选择要封装的算法和版本</li>
|
||||
<li><strong>配置API</strong>:设置API名称、路径、HTTP方法等</li>
|
||||
<li><strong>设置权限</strong>:配置是否需要认证、允许的角色等</li>
|
||||
<li><strong>流量限制</strong>:设置API的调用频率限制</li>
|
||||
<li><strong>测试API</strong>:使用测试功能验证API是否正常</li>
|
||||
<li><strong>发布API</strong>:将API设置为公开状态供他人使用</li>
|
||||
</ol>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="API配置说明" name="config">
|
||||
<div class="help-section">
|
||||
<h4>配置项说明:</h4>
|
||||
<el-table :data="configDescriptions" style="width: 100%">
|
||||
<el-table-column prop="item" label="配置项" width="150" />
|
||||
<el-table-column prop="description" label="说明" />
|
||||
<el-table-column prop="example" label="示例" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="常见问题" name="faq">
|
||||
<div class="help-section">
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="API调用失败怎么办?">
|
||||
<p>检查以下几点:</p>
|
||||
<ul>
|
||||
<li>关联的算法服务是否正在运行</li>
|
||||
<li>API路径是否正确</li>
|
||||
<li>请求参数格式是否正确</li>
|
||||
<li>是否有足够的权限调用API</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何设置API权限?">
|
||||
<p>权限配置包括:</p>
|
||||
<ul>
|
||||
<li><strong>是否需要认证</strong>:控制API是否需要用户登录</li>
|
||||
<li><strong>允许的角色</strong>:限制只有特定角色可以调用</li>
|
||||
<li><strong>是否公开</strong>:公开API不需要认证即可访问</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何设置流量限制?">
|
||||
<p>流量限制配置:</p>
|
||||
<ul>
|
||||
<li><strong>最大请求数</strong>:在时间窗口内的最大请求数</li>
|
||||
<li><strong>时间窗口</strong>:限制的时间范围(秒)</li>
|
||||
<li>示例:{"max_requests": 100, "window": 60} 表示每分钟最多100次请求</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何查看API调用统计?">
|
||||
<p>统计信息包括:</p>
|
||||
<ul>
|
||||
<li><strong>总调用次数</strong>:API被调用的总次数</li>
|
||||
<li><strong>成功次数</strong>:调用成功的次数</li>
|
||||
<li><strong>错误次数</strong>:调用失败的次数</li>
|
||||
<li><strong>平均响应时间</strong>:API的平均响应时间</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showHelpDialog = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -92,83 +319,146 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAlgorithmStore } from '../../stores/algorithm'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, Refresh, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { apiManagementService, type ApiEndpoint } from '../../services/apiManagement'
|
||||
|
||||
// 获取路由和存储
|
||||
const router = useRouter()
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 状态管理
|
||||
const apis = ref([])
|
||||
const algorithms = ref([])
|
||||
const versions = ref([])
|
||||
const apis = ref<ApiEndpoint[]>([])
|
||||
const algorithms = ref<any[]>([])
|
||||
const versions = ref<any[]>([])
|
||||
const showAddDialog = ref(false)
|
||||
const showTestDialog = ref(false)
|
||||
const showHelpDialog = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const filterStatus = ref('')
|
||||
const apiFormRef = ref()
|
||||
const testFormRef = ref()
|
||||
const testingApi = ref<ApiEndpoint | null>(null)
|
||||
const testing = ref(false)
|
||||
|
||||
const apiForm = ref({
|
||||
name: '',
|
||||
path: '',
|
||||
method: 'POST',
|
||||
algorithm_id: '',
|
||||
version_id: '',
|
||||
path: '',
|
||||
is_public: false,
|
||||
requires_auth: true,
|
||||
description: ''
|
||||
})
|
||||
|
||||
const testForm = ref({
|
||||
payload: '{"text":"测试数据","number":123}'
|
||||
})
|
||||
|
||||
const apiStats = ref({
|
||||
total_endpoints: 0,
|
||||
active_endpoints: 0,
|
||||
total_calls: '0',
|
||||
total_success: '0',
|
||||
total_errors: '0',
|
||||
avg_response_time: '0.00'
|
||||
})
|
||||
|
||||
const configDescriptions = ref([
|
||||
{
|
||||
item: 'API名称',
|
||||
description: 'API的显示名称,用于标识和区分不同的API',
|
||||
example: '图像分类API'
|
||||
},
|
||||
{
|
||||
item: 'API路径',
|
||||
description: 'API的访问路径,客户端通过此路径调用API',
|
||||
example: '/api/image-classification'
|
||||
},
|
||||
{
|
||||
item: 'HTTP方法',
|
||||
description: 'API支持的HTTP方法',
|
||||
example: 'POST'
|
||||
},
|
||||
{
|
||||
item: '是否公开',
|
||||
description: 'API是否对外公开,公开API不需要认证即可访问',
|
||||
example: 'true'
|
||||
},
|
||||
{
|
||||
item: '是否需要认证',
|
||||
description: 'API调用是否需要用户认证',
|
||||
example: 'true'
|
||||
}
|
||||
])
|
||||
|
||||
// 帮助说明相关状态
|
||||
const activeHelpSections = ref<string[]>(['what'])
|
||||
const currentStep = ref(0)
|
||||
|
||||
// 表单验证规则
|
||||
const apiRules = ref({
|
||||
name: [
|
||||
{ required: true, message: '请输入API名称', trigger: 'blur' }
|
||||
],
|
||||
path: [
|
||||
{ required: true, message: '请输入API路径', trigger: 'blur' }
|
||||
],
|
||||
method: [
|
||||
{ required: true, message: '请选择HTTP方法', trigger: 'blur' }
|
||||
],
|
||||
algorithm_id: [
|
||||
{ required: true, message: '请选择算法', trigger: 'blur' }
|
||||
],
|
||||
version_id: [
|
||||
{ required: true, message: '请选择版本', trigger: 'blur' }
|
||||
],
|
||||
path: [
|
||||
{ required: true, message: '请输入API路径', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 加载API列表
|
||||
const loadApis = async () => {
|
||||
// 这里应该调用后端API获取API列表
|
||||
// 暂时模拟数据
|
||||
apis.value = [
|
||||
{
|
||||
id: 'api-1',
|
||||
name: '图像分类API',
|
||||
algorithm_name: 'ResNet50',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 'api-2',
|
||||
name: '文本分类API',
|
||||
algorithm_name: 'BERT',
|
||||
version: '1.0.0',
|
||||
status: 'active',
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
try {
|
||||
const result = await apiManagementService.getApiEndpoints(filterStatus.value)
|
||||
apis.value = result.endpoints
|
||||
} catch (error) {
|
||||
console.error('加载API列表失败:', error)
|
||||
ElMessage.error('加载API列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载API统计
|
||||
const loadApiStats = async () => {
|
||||
try {
|
||||
const stats = await apiManagementService.getApiStats()
|
||||
apiStats.value = stats
|
||||
} catch (error) {
|
||||
console.error('加载API统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载算法列表
|
||||
const loadAlgorithms = async () => {
|
||||
await algorithmStore.fetchAlgorithms()
|
||||
algorithms.value = algorithmStore.algorithms
|
||||
try {
|
||||
await algorithmStore.fetchAlgorithms()
|
||||
algorithms.value = algorithmStore.algorithms
|
||||
} catch (error) {
|
||||
console.error('加载算法列表失败:', error)
|
||||
ElMessage.error('加载算法列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载版本列表
|
||||
const loadVersions = async (algorithmId: string) => {
|
||||
if (algorithmId) {
|
||||
const algorithm = algorithmStore.algorithms.find(a => a.id === algorithmId)
|
||||
const algorithm = algorithmStore.algorithms.find((a: any) => a.id === algorithmId)
|
||||
if (algorithm && algorithm.versions) {
|
||||
versions.value = algorithm.versions
|
||||
}
|
||||
@@ -184,51 +474,141 @@ const handleAlgorithmChange = (algorithmId: string) => {
|
||||
const addApi = async () => {
|
||||
if (!apiFormRef.value) return
|
||||
|
||||
// 验证表单
|
||||
await apiFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
// 这里应该调用后端API添加API
|
||||
// 暂时模拟添加
|
||||
console.log('添加API:', apiForm.value)
|
||||
ElMessage.success('API封装成功')
|
||||
|
||||
// 关闭对话框
|
||||
showAddDialog.value = false
|
||||
|
||||
// 重置表单
|
||||
apiForm.value = {
|
||||
name: '',
|
||||
algorithm_id: '',
|
||||
version_id: '',
|
||||
path: '',
|
||||
description: ''
|
||||
try {
|
||||
await apiManagementService.createApiEndpoint(apiForm.value)
|
||||
ElMessage.success('API封装成功')
|
||||
closeDialog()
|
||||
await loadApis()
|
||||
await loadApiStats()
|
||||
} catch (error: any) {
|
||||
console.error('添加API失败:', error)
|
||||
ElMessage.error(error.message || '添加API失败')
|
||||
}
|
||||
|
||||
// 重新加载API列表
|
||||
await loadApis()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑API
|
||||
const editApi = (api: any) => {
|
||||
// 这里应该打开编辑对话框,暂时打印
|
||||
console.log('编辑API:', api)
|
||||
const editApi = (api: ApiEndpoint) => {
|
||||
isEditMode.value = true
|
||||
apiForm.value = {
|
||||
name: api.name,
|
||||
path: api.path,
|
||||
method: api.method,
|
||||
algorithm_id: api.algorithm_id,
|
||||
version_id: api.version_id,
|
||||
is_public: api.is_public,
|
||||
requires_auth: true,
|
||||
description: api.description
|
||||
}
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
// 更新API
|
||||
const updateApi = async () => {
|
||||
if (!apiFormRef.value) return
|
||||
|
||||
await apiFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const apiId = apis.value.find((a: ApiEndpoint) => a.name === apiForm.value.name)?.id || ''
|
||||
await apiManagementService.updateApiEndpoint(apiId, apiForm.value)
|
||||
ElMessage.success('API更新成功')
|
||||
closeDialog()
|
||||
await loadApis()
|
||||
} catch (error: any) {
|
||||
console.error('更新API失败:', error)
|
||||
ElMessage.error(error.message || '更新API失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存API
|
||||
const saveApi = async () => {
|
||||
if (isEditMode.value) {
|
||||
await updateApi()
|
||||
} else {
|
||||
await addApi()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除API
|
||||
const deleteApi = (apiId: string) => {
|
||||
// 这里应该调用后端API删除API,暂时打印
|
||||
console.log('删除API:', apiId)
|
||||
ElMessage.success('API删除成功')
|
||||
// 重新加载API列表
|
||||
loadApis()
|
||||
const deleteApi = async (apiId: string) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除此API吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await apiManagementService.deleteApiEndpoint(apiId)
|
||||
ElMessage.success('API删除成功')
|
||||
await loadApis()
|
||||
await loadApiStats()
|
||||
} catch (error: any) {
|
||||
console.error('删除API失败:', error)
|
||||
ElMessage.error(error.message || '删除API失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 测试API
|
||||
const testApi = (api: ApiEndpoint) => {
|
||||
testingApi.value = api
|
||||
testForm.value.payload = '{"text":"测试数据","number":123}'
|
||||
showTestDialog.value = true
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
const executeTest = async () => {
|
||||
if (!testFormRef.value || !testingApi.value) return
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(testForm.value.payload)
|
||||
testing.value = true
|
||||
|
||||
const result = await apiManagementService.testApiEndpoint(testingApi.value.id, payload)
|
||||
|
||||
if (result.success) {
|
||||
ElMessage.success(`API测试成功,响应时间:${result.response_time.toFixed(2)}秒`)
|
||||
ElMessage.info(`响应结果:${JSON.stringify(result.result)}`)
|
||||
} else {
|
||||
ElMessage.error(`API测试失败:${result.error}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('测试API失败:', error)
|
||||
ElMessage.error(error.message || '测试API失败')
|
||||
} finally {
|
||||
testing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const closeDialog = () => {
|
||||
showAddDialog.value = false
|
||||
isEditMode.value = false
|
||||
apiForm.value = {
|
||||
name: '',
|
||||
path: '',
|
||||
method: 'POST',
|
||||
algorithm_id: '',
|
||||
version_id: '',
|
||||
is_public: false,
|
||||
requires_auth: true,
|
||||
description: ''
|
||||
}
|
||||
if (apiFormRef.value) {
|
||||
apiFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(async () => {
|
||||
await loadAlgorithms()
|
||||
await loadApis()
|
||||
await loadApiStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -247,7 +627,100 @@ onMounted(async () => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-bar .el-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.api-info {
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.api-info p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.help-section h4 {
|
||||
margin: 15px 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.help-section p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section ul,
|
||||
.help-section ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-section li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section strong {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-dialog-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-cards {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -259,5 +732,13 @@ onMounted(async () => {
|
||||
.action-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-bar .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
618
frontend/src/views/admin/AdminConfigManagementView.vue
Normal file
618
frontend/src/views/admin/AdminConfigManagementView.vue
Normal file
@@ -0,0 +1,618 @@
|
||||
<template>
|
||||
<div class="config-management-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>配置管理</h1>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="showAddDialog = true">
|
||||
<el-icon><plus /></el-icon>
|
||||
添加配置
|
||||
</el-button>
|
||||
<el-button @click="loadConfigs">刷新</el-button>
|
||||
<el-button type="info" @click="showHelpDialog = true">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
帮助说明
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="filterType" placeholder="配置类型" clearable @change="loadConfigs">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="系统配置" value="system" />
|
||||
<el-option label="服务配置" value="service" />
|
||||
<el-option label="用户配置" value="user" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 配置列表 -->
|
||||
<el-table :data="configs" style="width: 100%">
|
||||
<el-table-column prop="config_key" label="配置键" width="250" />
|
||||
<el-table-column prop="config_value" label="配置值" width="300" />
|
||||
<el-table-column prop="config_type" label="类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getConfigTypeTag(scope.row.config_type)">
|
||||
{{ getConfigTypeName(scope.row.config_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="service_id" label="服务ID" width="150" />
|
||||
<el-table-column prop="description" label="描述" width="200" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editConfig(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteConfig(scope.row.config_key)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑配置对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
:title="isEditMode ? '编辑配置' : '添加配置'"
|
||||
width="60%"
|
||||
>
|
||||
<el-form :model="configForm" :rules="configRules" ref="configFormRef">
|
||||
<el-form-item label="配置键" prop="config_key">
|
||||
<el-input
|
||||
v-model="configForm.config_key"
|
||||
placeholder="请输入配置键"
|
||||
:disabled="isEditMode"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置值" prop="config_value">
|
||||
<el-input
|
||||
v-model="configForm.config_value"
|
||||
placeholder="请输入配置值"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="配置类型" prop="config_type">
|
||||
<el-select v-model="configForm.config_type" placeholder="选择配置类型">
|
||||
<el-option label="系统配置" value="system" />
|
||||
<el-option label="服务配置" value="service" />
|
||||
<el-option label="用户配置" value="user" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="服务ID" prop="service_id">
|
||||
<el-input
|
||||
v-model="configForm.service_id"
|
||||
placeholder="请输入服务ID(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="configForm.description"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入配置描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveConfig">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 帮助说明对话框 -->
|
||||
<el-dialog
|
||||
v-model="showHelpDialog"
|
||||
title="配置管理说明"
|
||||
width="70%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="help-dialog-content">
|
||||
<el-collapse v-model="activeHelpSections">
|
||||
<el-collapse-item title="什么是配置管理?" name="what">
|
||||
<div class="help-section">
|
||||
<p><strong>配置管理</strong>是智能算法展示平台的核心功能,用于管理系统、服务和用户的各种配置项。</p>
|
||||
<p>它提供了统一的配置管理界面,支持不同层级的配置,采用三层配置架构,确保配置的灵活性和安全性。</p>
|
||||
<el-divider />
|
||||
<h4>主要功能:</h4>
|
||||
<ul>
|
||||
<li><strong>配置存储</strong>:将配置存储在数据库中,便于管理和查询</li>
|
||||
<li><strong>配置分层</strong>:支持系统、服务、用户三层配置管理</li>
|
||||
<li><strong>动态更新</strong>:配置修改后立即生效,无需重启服务</li>
|
||||
<li><strong>配置优先级</strong>:环境变量 > 数据库 > 文件默认值</li>
|
||||
<li><strong>配置历史</strong>:记录配置的创建和更新时间</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="为什么需要配置管理?" name="why">
|
||||
<div class="help-section">
|
||||
<h4>解决的问题:</h4>
|
||||
<ul>
|
||||
<li><strong>配置集中管理</strong>:避免配置分散在多个文件中</li>
|
||||
<li><strong>动态配置</strong>:无需重启服务即可修改配置</li>
|
||||
<li><strong>配置安全</strong>:敏感配置可以单独管理,不暴露在代码中</li>
|
||||
<li><strong>环境适配</strong>:不同环境使用不同配置,便于部署</li>
|
||||
<li><strong>配置版本</strong>:记录配置变更历史,便于追溯</li>
|
||||
</ul>
|
||||
<el-divider />
|
||||
<h4>应用场景:</h4>
|
||||
<ul>
|
||||
<li><strong>系统配置</strong>:数据库连接、Redis配置、API密钥等</li>
|
||||
<li><strong>服务配置</strong>:算法服务的超时时间、并发数等</li>
|
||||
<li><strong>用户配置</strong>:用户偏好、权限限制等</li>
|
||||
<li><strong>环境切换</strong>:开发、测试、生产环境配置切换</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何使用配置管理?" name="how">
|
||||
<div class="help-section">
|
||||
<h4>使用流程:</h4>
|
||||
<el-steps :active="currentStep" finish-status="success" align-center>
|
||||
<el-step title="确定配置类型" description="选择系统/服务/用户配置" />
|
||||
<el-step title="添加配置" description="填写配置键、值和描述" />
|
||||
<el-step title="应用配置" description="配置立即生效" />
|
||||
<el-step title="管理配置" description="编辑、删除或查看配置" />
|
||||
</el-steps>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>详细步骤:</h4>
|
||||
<ol>
|
||||
<li><strong>确定配置类型</strong>:根据配置的用途选择合适的类型(system/service/user)</li>
|
||||
<li><strong>添加配置</strong>:点击"添加配置"按钮,填写配置信息</li>
|
||||
<li><strong>配置键命名</strong>:使用命名空间规范,如"openai.api_config"</li>
|
||||
<li><strong>配置值格式</strong>:使用JSON格式,便于存储复杂配置</li>
|
||||
<li><strong>应用配置</strong>:配置保存后立即生效,无需重启</li>
|
||||
<li><strong>管理配置</strong>:可以随时编辑、删除或查看配置历史</li>
|
||||
</ol>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="配置类型说明" name="types">
|
||||
<div class="help-section">
|
||||
<h4>配置类型:</h4>
|
||||
<el-table :data="configTypeDescriptions" style="width: 100%">
|
||||
<el-table-column prop="type" label="类型" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getConfigTypeTag(scope.row.type)">
|
||||
{{ getConfigTypeName(scope.row.type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" />
|
||||
<el-table-column prop="examples" label="示例" />
|
||||
<el-table-column prop="scope" label="作用范围" width="120" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="配置命名规范" name="naming">
|
||||
<div class="help-section">
|
||||
<h4>命名规范:</h4>
|
||||
<ul>
|
||||
<li><strong>使用小写字母</strong>:配置键使用小写字母和点号分隔</li>
|
||||
<li><strong>使用命名空间</strong>:按功能模块组织配置</li>
|
||||
<li><strong>使用下划线</strong>:单词之间使用下划线连接</li>
|
||||
<li><strong>避免特殊字符</strong>:不要使用空格和特殊符号</li>
|
||||
</ul>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>命名示例:</h4>
|
||||
<el-table :data="namingExamples" style="width: 100%">
|
||||
<el-table-column prop="category" label="类别" width="150" />
|
||||
<el-table-column prop="example" label="示例" />
|
||||
<el-table-column prop="description" label="说明" />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="常见问题" name="faq">
|
||||
<div class="help-section">
|
||||
<el-collapse accordion>
|
||||
<el-collapse-item title="配置修改后多久生效?">
|
||||
<p>配置修改后<strong>立即生效</strong>,无需重启服务。系统会自动从数据库读取最新配置。</p>
|
||||
<p>注意:某些配置可能需要服务重新加载才能生效,具体取决于配置的使用方式。</p>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何备份配置?">
|
||||
<p>配置存储在数据库中,建议:</p>
|
||||
<ul>
|
||||
<li>定期备份数据库</li>
|
||||
<li>记录重要配置的变更历史</li>
|
||||
<li>使用版本控制管理配置文件</li>
|
||||
<li>在修改配置前先导出现有配置</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="配置值支持哪些格式?">
|
||||
<p>配置值支持<strong>JSON格式</strong>,可以存储:</p>
|
||||
<ul>
|
||||
<li>字符串:`"value"`</li>
|
||||
<li>数字:`123` 或 `123.45`</li>
|
||||
<li>布尔值:`true` 或 `false`</li>
|
||||
<li>数组:`["item1", "item2"]`</li>
|
||||
<li>对象:`{"key": "value", "num": 123}`</li>
|
||||
<li>嵌套结构:`{"config": {"nested": {"value": "test"}}}`</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="如何处理敏感配置?">
|
||||
<p>对于敏感配置(如API密钥、密码等),建议:</p>
|
||||
<ul>
|
||||
<li>使用环境变量存储,不要存储在数据库中</li>
|
||||
<li>在代码中使用占位符,运行时从环境变量读取</li>
|
||||
<li>定期轮换敏感配置</li>
|
||||
<li>限制敏感配置的访问权限</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="配置优先级是如何工作的?">
|
||||
<p>系统采用三层配置架构,优先级从高到低:</p>
|
||||
<ol>
|
||||
<li><strong>环境变量</strong>(最高优先级):适合敏感信息和部署环境配置</li>
|
||||
<li><strong>数据库配置</strong>(中等优先级):适合运行时动态配置</li>
|
||||
<li><strong>文件默认值</strong>(最低优先级):代码中的默认配置</li>
|
||||
</ol>
|
||||
<p>当多个配置源存在相同配置键时,优先级高的会覆盖优先级低的。</p>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showHelpDialog = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Plus, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { configService, type ConfigItem } from '../../services/admin'
|
||||
|
||||
// 状态管理
|
||||
const configs = ref<ConfigItem[]>([])
|
||||
const filterType = ref('')
|
||||
const showAddDialog = ref(false)
|
||||
const showHelpDialog = ref(false)
|
||||
const isEditMode = ref(false)
|
||||
const configFormRef = ref()
|
||||
const configForm = ref({
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
config_type: 'system',
|
||||
service_id: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 帮助说明相关状态
|
||||
const activeHelpSections = ref<string[]>(['what'])
|
||||
const currentStep = ref(0)
|
||||
|
||||
// 配置类型说明数据
|
||||
const configTypeDescriptions = ref([
|
||||
{
|
||||
type: 'system',
|
||||
description: '系统级别的全局配置,影响整个平台',
|
||||
examples: '数据库连接、Redis配置、API密钥等',
|
||||
scope: '全局'
|
||||
},
|
||||
{
|
||||
type: 'service',
|
||||
description: '特定算法服务的配置,只影响单个服务',
|
||||
examples: '超时时间、并发数、模型参数等',
|
||||
scope: '服务'
|
||||
},
|
||||
{
|
||||
type: 'user',
|
||||
description: '用户级别的配置,个性化设置',
|
||||
examples: '用户偏好、权限限制、存储配额等',
|
||||
scope: '用户'
|
||||
}
|
||||
])
|
||||
|
||||
// 命名示例数据
|
||||
const namingExamples = ref([
|
||||
{
|
||||
category: '系统配置',
|
||||
example: 'database.connection_pool',
|
||||
description: '数据库连接池配置'
|
||||
},
|
||||
{
|
||||
category: '系统配置',
|
||||
example: 'redis.cache_timeout',
|
||||
description: 'Redis缓存超时时间'
|
||||
},
|
||||
{
|
||||
category: '服务配置',
|
||||
example: 'service.{service_id}.timeout',
|
||||
description: '特定服务的超时配置'
|
||||
},
|
||||
{
|
||||
category: '服务配置',
|
||||
example: 'service.{service_id}.max_concurrent',
|
||||
description: '特定服务的最大并发数'
|
||||
},
|
||||
{
|
||||
category: '用户配置',
|
||||
example: 'user.{user_id}.preferences',
|
||||
description: '用户偏好设置'
|
||||
},
|
||||
{
|
||||
category: '用户配置',
|
||||
example: 'user.{user_id}.permissions',
|
||||
description: '用户权限配置'
|
||||
}
|
||||
])
|
||||
|
||||
// 表单验证规则
|
||||
const configRules = ref({
|
||||
config_key: [
|
||||
{ required: true, message: '请输入配置键', trigger: 'blur' }
|
||||
],
|
||||
config_value: [
|
||||
{ required: true, message: '请输入配置值', trigger: 'blur' }
|
||||
],
|
||||
config_type: [
|
||||
{ required: true, message: '请选择配置类型', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 获取配置类型标签样式
|
||||
const getConfigTypeTag = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'system': 'danger',
|
||||
'service': 'warning',
|
||||
'user': 'success'
|
||||
}
|
||||
return typeMap[type] || 'info'
|
||||
}
|
||||
|
||||
// 获取配置类型名称
|
||||
const getConfigTypeName = (type: string) => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'system': '系统配置',
|
||||
'service': '服务配置',
|
||||
'user': '用户配置'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 加载配置列表
|
||||
const loadConfigs = async () => {
|
||||
try {
|
||||
configs.value = await configService.getAllConfigs(filterType.value)
|
||||
} catch (error) {
|
||||
console.error('加载配置列表失败:', error)
|
||||
ElMessage.error('加载配置列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 添加配置
|
||||
const addConfig = async () => {
|
||||
try {
|
||||
const success = await configService.setConfig(configForm.value.config_key, {
|
||||
value: configForm.value.config_value,
|
||||
type: configForm.value.config_type,
|
||||
service_id: configForm.value.service_id || null,
|
||||
description: configForm.value.description
|
||||
})
|
||||
|
||||
if (success) {
|
||||
ElMessage.success('配置添加成功')
|
||||
closeDialog()
|
||||
loadConfigs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('添加配置失败:', error)
|
||||
ElMessage.error('添加配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑配置
|
||||
const editConfig = (config: any) => {
|
||||
isEditMode.value = true
|
||||
configForm.value = {
|
||||
config_key: config.config_key,
|
||||
config_value: config.config_value,
|
||||
config_type: config.config_type,
|
||||
service_id: config.service_id || '',
|
||||
description: config.description || ''
|
||||
}
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
const updateConfig = async () => {
|
||||
try {
|
||||
const success = await configService.setConfig(configForm.value.config_key, {
|
||||
value: configForm.value.config_value,
|
||||
type: configForm.value.config_type,
|
||||
service_id: configForm.value.service_id || null,
|
||||
description: configForm.value.description
|
||||
})
|
||||
|
||||
if (success) {
|
||||
ElMessage.success('配置更新成功')
|
||||
closeDialog()
|
||||
loadConfigs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新配置失败:', error)
|
||||
ElMessage.error('更新配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
if (!configFormRef.value) return
|
||||
|
||||
await configFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
if (isEditMode.value) {
|
||||
await updateConfig()
|
||||
} else {
|
||||
await addConfig()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const deleteConfig = (configKey: string) => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要删除此配置吗?',
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const success = await configService.deleteConfig(configKey)
|
||||
|
||||
if (success) {
|
||||
ElMessage.success('配置删除成功')
|
||||
loadConfigs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
ElMessage.error('删除配置失败')
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const closeDialog = () => {
|
||||
showAddDialog.value = false
|
||||
isEditMode.value = false
|
||||
configForm.value = {
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
config_type: 'system',
|
||||
service_id: '',
|
||||
description: ''
|
||||
}
|
||||
if (configFormRef.value) {
|
||||
configFormRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(() => {
|
||||
loadConfigs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-management-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-management-container h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.help-section {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.help-section h4 {
|
||||
margin: 15px 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.help-section p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section ul,
|
||||
.help-section ol {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-section li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.help-section strong {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.help-dialog-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-bar .el-select {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-table-column {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filter-bar .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -104,6 +104,38 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 算法技术分类 -->
|
||||
<el-form-item label="技术分类" prop="tech_category">
|
||||
<el-select
|
||||
v-model="serviceForm.tech_category"
|
||||
placeholder="请选择技术分类"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option label="计算机视觉" value="computer_vision" />
|
||||
<el-option label="视频处理" value="video_processing" />
|
||||
<el-option label="自然语言处理" value="nlp" />
|
||||
<el-option label="机器学习" value="ml" />
|
||||
<el-option label="边缘计算" value="edge_computing" />
|
||||
<el-option label="医疗算法" value="medical" />
|
||||
<el-option label="自动驾驶算法" value="autonomous_driving" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 输出类型 -->
|
||||
<el-form-item label="输出类型" prop="output_type">
|
||||
<el-select
|
||||
v-model="serviceForm.output_type"
|
||||
placeholder="请选择输出类型"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option label="图片" value="image" />
|
||||
<el-option label="视频" value="video" />
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="JSON" value="json" />
|
||||
<el-option label="音频" value="audio" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 服务配置 -->
|
||||
<el-form-item label="服务类型" prop="service_type">
|
||||
<el-select
|
||||
@@ -292,6 +324,8 @@ const serviceForm = reactive({
|
||||
name: '',
|
||||
version: '1.0.0',
|
||||
description: '',
|
||||
tech_category: 'computer_vision',
|
||||
output_type: 'image',
|
||||
service_type: 'http',
|
||||
host: '0.0.0.0',
|
||||
port: 8000,
|
||||
@@ -333,7 +367,7 @@ const rules = {
|
||||
const showResultDialog = ref(false)
|
||||
const registrationResult = reactive({
|
||||
success: false,
|
||||
service: null,
|
||||
service: null as any,
|
||||
error: ''
|
||||
})
|
||||
|
||||
@@ -434,6 +468,9 @@ const submitForm = async () => {
|
||||
repository_id: serviceForm.repository_id,
|
||||
name: serviceForm.name,
|
||||
version: serviceForm.version,
|
||||
description: serviceForm.description,
|
||||
tech_category: serviceForm.tech_category,
|
||||
output_type: serviceForm.output_type,
|
||||
service_type: serviceForm.service_type,
|
||||
host: serviceForm.host,
|
||||
port: serviceForm.port,
|
||||
@@ -472,6 +509,8 @@ const resetForm = () => {
|
||||
}
|
||||
// 重置默认值
|
||||
serviceForm.version = '1.0.0'
|
||||
serviceForm.tech_category = 'computer_vision'
|
||||
serviceForm.output_type = 'image'
|
||||
serviceForm.service_type = 'http'
|
||||
serviceForm.host = '0.0.0.0'
|
||||
serviceForm.port = 8000
|
||||
|
||||
@@ -144,14 +144,26 @@ const fetchRoles = async () => {
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
// 检查用户是否已登录
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
console.log('用户未登录,跳过加载用户列表')
|
||||
error.value = '请先登录'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const response = await axios.get('/api/users/')
|
||||
users.value = response.data.users
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('获取用户列表失败:', err)
|
||||
error.value = '获取用户列表失败'
|
||||
if (err.response?.status === 401 || err.response?.status === 403) {
|
||||
error.value = '权限不足,请重新登录'
|
||||
} else {
|
||||
error.value = '获取用户列表失败'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
467
frontend/test.html
Normal file
467
frontend/test.html
Normal file
@@ -0,0 +1,467 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>系统功能测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #409eff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.test-section {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.test-section h2 {
|
||||
color: #409eff;
|
||||
margin-top: 0;
|
||||
}
|
||||
.test-item {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.test-item.success {
|
||||
border-left: 4px solid #67c23a;
|
||||
}
|
||||
.test-item.error {
|
||||
border-left: 4px solid #f56c6c;
|
||||
}
|
||||
.test-item.pending {
|
||||
border-left: 4px solid #e6a23c;
|
||||
}
|
||||
.status {
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.success .status {
|
||||
color: #67c23a;
|
||||
}
|
||||
.error .status {
|
||||
color: #f56c6c;
|
||||
}
|
||||
.pending .status {
|
||||
color: #e6a23c;
|
||||
}
|
||||
button {
|
||||
background-color: #409eff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #66b1ff;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.result {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.summary {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: #e6f7ff;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.summary.success {
|
||||
background-color: #f0f9ff;
|
||||
color: #67c23a;
|
||||
}
|
||||
.summary.error {
|
||||
background-color: #fef0f0;
|
||||
color: #f56c6c;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>系统功能自动化测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>测试控制</h2>
|
||||
<button id="runAllTests" onclick="runAllTests()">运行所有测试</button>
|
||||
<button id="resetTests" onclick="resetTests()">重置测试</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>登录测试</h2>
|
||||
<div class="test-item pending" id="loginTest">
|
||||
<span>测试用户登录</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="loginResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>配置管理API测试</h2>
|
||||
<div class="test-item pending" id="getConfigsTest">
|
||||
<span>获取所有配置</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getConfigsResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="addConfigTest">
|
||||
<span>添加测试配置</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="addConfigResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="getConfigTest">
|
||||
<span>获取单个配置</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getConfigResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="deleteConfigTest">
|
||||
<span>删除测试配置</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="deleteConfigResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>算法比较API测试</h2>
|
||||
<div class="test-item pending" id="compareAlgorithmsTest">
|
||||
<span>算法效果比较</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="compareAlgorithmsResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>现有API测试</h2>
|
||||
<div class="test-item pending" id="healthCheckTest">
|
||||
<span>健康检查</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="healthCheckResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="getCurrentUserTest">
|
||||
<span>获取当前用户</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getCurrentUserResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="getAlgorithmsTest">
|
||||
<span>获取算法列表</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getAlgorithmsResult"></div>
|
||||
</div>
|
||||
<div class="test-item pending" id="getServicesTest">
|
||||
<span>获取服务列表</span>
|
||||
<span class="status">待测试</span>
|
||||
<div class="result" id="getServicesResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary" id="testSummary">
|
||||
点击"运行所有测试"开始测试
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = 'http://localhost:8001/api/v1';
|
||||
let authToken = null;
|
||||
let testResults = {
|
||||
total: 0,
|
||||
passed: 0,
|
||||
failed: 0
|
||||
};
|
||||
|
||||
async function makeRequest(url, options = {}) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const data = await response.json();
|
||||
return { success: response.ok, status: response.status, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
function updateTestItem(testId, success, message) {
|
||||
const testItem = document.getElementById(testId);
|
||||
const resultDiv = document.getElementById(testId + 'Result');
|
||||
|
||||
testItem.classList.remove('pending', 'success', 'error');
|
||||
testItem.classList.add(success ? 'success' : 'error');
|
||||
|
||||
const statusSpan = testItem.querySelector('.status');
|
||||
statusSpan.textContent = success ? '✓ 通过' : '✗ 失败';
|
||||
|
||||
resultDiv.textContent = message;
|
||||
|
||||
testResults.total++;
|
||||
if (success) {
|
||||
testResults.passed++;
|
||||
} else {
|
||||
testResults.failed++;
|
||||
}
|
||||
}
|
||||
|
||||
async function testLogin() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/users/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'admin', password: 'admin123' })
|
||||
});
|
||||
|
||||
if (result.success && result.data.access_token) {
|
||||
authToken = result.data.access_token;
|
||||
updateTestItem('loginTest', true, `登录成功,用户ID: ${result.data.user_id}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('loginTest', false, `登录失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetConfigs() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/config/`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const configs = result.data.configs || [];
|
||||
updateTestItem('getConfigsTest', true, `获取成功,配置数量: ${configs.length}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getConfigsTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testAddConfig() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/config/test_frontend_config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
value: 'frontend_test_value',
|
||||
type: 'system',
|
||||
service_id: null,
|
||||
description: '前端测试配置'
|
||||
})
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('addConfigTest', true, '添加配置成功');
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('addConfigTest', false, `添加失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetConfig() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/config/test_frontend_config`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('getConfigTest', true, `获取成功,配置值: ${result.data.value}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getConfigTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testDeleteConfig() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/config/test_frontend_config`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('deleteConfigTest', true, '删除配置成功');
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('deleteConfigTest', false, `删除失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testCompareAlgorithms() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/comparison/compare-algorithms`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input_data: { text: '前端测试文本' },
|
||||
algorithm_configs: [
|
||||
{
|
||||
algorithm_id: 'frontend_test_1',
|
||||
algorithm_name: '前端测试算法1',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
},
|
||||
{
|
||||
algorithm_id: 'frontend_test_2',
|
||||
algorithm_name: '前端测试算法2',
|
||||
version: '1.0.0',
|
||||
config: '{}'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
if (result.success && result.data.success) {
|
||||
const resultsCount = result.data.results ? result.data.results.length : 0;
|
||||
updateTestItem('compareAlgorithmsTest', true, `比较成功,结果数量: ${resultsCount}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('compareAlgorithmsTest', false, `比较失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testHealthCheck() {
|
||||
const result = await makeRequest(`${API_BASE_URL.replace('/api/v1', '')}/health`);
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('healthCheckTest', true, `健康检查通过: ${result.data.status}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('healthCheckTest', false, `健康检查失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetCurrentUser() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/users/me`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
updateTestItem('getCurrentUserTest', true, `获取成功,用户名: ${result.data.username}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getCurrentUserTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetAlgorithms() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/algorithms/`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const count = Array.isArray(result.data) ? result.data.length : 0;
|
||||
updateTestItem('getAlgorithmsTest', true, `获取成功,算法数量: ${count}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getAlgorithmsTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetServices() {
|
||||
const result = await makeRequest(`${API_BASE_URL}/services`, {
|
||||
headers: { 'Authorization': `Bearer ${authToken}` }
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const count = Array.isArray(result.data) ? result.data.length : 0;
|
||||
updateTestItem('getServicesTest', true, `获取成功,服务数量: ${count}`);
|
||||
return true;
|
||||
} else {
|
||||
updateTestItem('getServicesTest', false, `获取失败: ${result.error || result.data.detail}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
// 重置测试结果
|
||||
testResults = { total: 0, passed: 0, failed: 0 };
|
||||
|
||||
// 禁用按钮
|
||||
document.getElementById('runAllTests').disabled = true;
|
||||
document.getElementById('resetTests').disabled = true;
|
||||
|
||||
// 运行测试
|
||||
await testLogin();
|
||||
|
||||
if (authToken) {
|
||||
await testHealthCheck();
|
||||
await testGetCurrentUser();
|
||||
await testGetAlgorithms();
|
||||
await testGetServices();
|
||||
await testGetConfigs();
|
||||
await testAddConfig();
|
||||
await testGetConfig();
|
||||
await testDeleteConfig();
|
||||
await testCompareAlgorithms();
|
||||
}
|
||||
|
||||
// 更新摘要
|
||||
const summaryDiv = document.getElementById('testSummary');
|
||||
summaryDiv.classList.remove('success', 'error');
|
||||
|
||||
if (testResults.failed === 0) {
|
||||
summaryDiv.classList.add('success');
|
||||
summaryDiv.textContent = `🎉 所有测试通过!总计: ${testResults.passed}/${testResults.total}`;
|
||||
} else {
|
||||
summaryDiv.classList.add('error');
|
||||
summaryDiv.textContent = `⚠️ 部分测试失败!总计: ${testResults.passed}/${testResults.total},失败: ${testResults.failed}`;
|
||||
}
|
||||
|
||||
// 启用按钮
|
||||
document.getElementById('runAllTests').disabled = false;
|
||||
document.getElementById('resetTests').disabled = false;
|
||||
}
|
||||
|
||||
function resetTests() {
|
||||
// 重置所有测试项
|
||||
const testItems = document.querySelectorAll('.test-item');
|
||||
testItems.forEach(item => {
|
||||
item.classList.remove('success', 'error');
|
||||
item.classList.add('pending');
|
||||
const statusSpan = item.querySelector('.status');
|
||||
statusSpan.textContent = '待测试';
|
||||
const resultDiv = item.querySelector('.result');
|
||||
resultDiv.textContent = '';
|
||||
});
|
||||
|
||||
// 重置摘要
|
||||
const summaryDiv = document.getElementById('testSummary');
|
||||
summaryDiv.classList.remove('success', 'error');
|
||||
summaryDiv.textContent = '点击"运行所有测试"开始测试';
|
||||
|
||||
// 重置结果
|
||||
testResults = { total: 0, passed: 0, failed: 0 };
|
||||
authToken = null;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -14,8 +14,24 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: 'http://localhost:8001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api/v1'),
|
||||
timeout: 600000 // 10分钟超时
|
||||
rewrite: (path) => {
|
||||
// 如果路径已经是 /api/v1/ 开头,则不重写
|
||||
if (path.startsWith('/api/v1/')) {
|
||||
return path
|
||||
}
|
||||
// 否则将 /api/ 重写为 /api/v1/
|
||||
return path.replace(/^\/api\//, '/api/v1/')
|
||||
},
|
||||
timeout: 600000, // 10分钟超时
|
||||
// 确保认证头在重定向时被保留
|
||||
configure: (proxy, options) => {
|
||||
proxy.on('proxyReq', (proxyReq, req, res) => {
|
||||
// 确保认证头被正确传递
|
||||
if (req.headers.authorization) {
|
||||
proxyReq.setHeader('Authorization', req.headers.authorization)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user