good version for 算法注册

This commit is contained in:
2026-02-15 21:23:28 +08:00
parent 3c03777b97
commit 62ea5d36a5
115 changed files with 9566 additions and 1576 deletions

View 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>

View File

@@ -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>

View File

@@ -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('登录已过期,请重新登录')

View File

@@ -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: '算法效果比较'
}
}
]
},

View 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()

View 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()
}
}

View 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

View File

@@ -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 || '创建算法失败'

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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>

View File

@@ -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;
}
}

View 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>

View File

@@ -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;

View File

@@ -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)
}
}
}

View File

@@ -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>

View 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>

View File

@@ -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

View File

@@ -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
View 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>

View File

@@ -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)
}
})
}
}
}
},