first commit
This commit is contained in:
172
frontend/src/App.vue
Normal file
172
frontend/src/App.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<router-link to="/" class="logo">
|
||||
智能算法展示平台
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<el-menu :default-active="activeMenu" mode="horizontal" background-color="#fff" text-color="#333" active-text-color="#409EFF">
|
||||
<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>
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/admin">
|
||||
<router-link to="/admin">管理员中心</router-link>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<template v-if="userStore.isLoggedIn">
|
||||
<el-dropdown>
|
||||
<span class="user-info">
|
||||
{{ userStore.user?.username }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-link to="/login" class="login-btn">登录</router-link>
|
||||
<router-link to="/register" class="register-btn">注册</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<footer class="app-footer">
|
||||
<p>© 2026 智能算法展示平台. 保留所有权利.</p>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from './stores/user'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
|
||||
// 获取用户存储
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 计算当前激活的菜单
|
||||
const activeMenu = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
// 处理退出登录
|
||||
const handleLogout = () => {
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
// 初始化用户状态
|
||||
onMounted(() => {
|
||||
userStore.init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
flex: 1;
|
||||
margin: 0 40px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-btn,
|
||||
.register-btn {
|
||||
margin-left: 15px;
|
||||
text-decoration: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
color: #409EFF;
|
||||
border: 1px solid #409EFF;
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
color: #fff;
|
||||
background-color: #409EFF;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
height: 60px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
1
frontend/src/assets/vue.svg
Normal file
1
frontend/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
41
frontend/src/components/HelloWorld.vue
Normal file
41
frontend/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
400
frontend/src/components/SimulationInputGenerator.vue
Normal file
400
frontend/src/components/SimulationInputGenerator.vue
Normal file
@@ -0,0 +1,400 @@
|
||||
<template>
|
||||
<div class="simulation-input-generator">
|
||||
<el-card class="input-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>仿真输入生成器</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="生成方式">
|
||||
<el-radio-group v-model="form.method" @change="handleMethodChange">
|
||||
<el-radio label="openai">通过OpenAI生成</el-radio>
|
||||
<el-radio label="template">使用预设模板</el-radio>
|
||||
<el-radio label="manual">手动输入</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- OpenAI生成选项 -->
|
||||
<div v-if="form.method === 'openai'">
|
||||
<el-form-item label="描述提示">
|
||||
<el-input
|
||||
v-model="form.prompt"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="描述你想要的输入数据,例如:'一张包含猫和狗的照片' 或 '包含年龄、姓名、邮箱的用户数据'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="数据类型">
|
||||
<el-select v-model="form.dataType" placeholder="请选择数据类型">
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="结构化数据" value="structured" />
|
||||
<el-option label="图像描述" value="image" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="generateWithOpenAI"
|
||||
:disabled="!form.prompt"
|
||||
>
|
||||
生成仿真数据
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 预设模板选项 -->
|
||||
<div v-if="form.method === 'template'">
|
||||
<el-form-item label="选择模板">
|
||||
<el-select v-model="form.selectedTemplate" placeholder="请选择模板" @change="loadTemplateData">
|
||||
<el-option
|
||||
v-for="template in templates"
|
||||
:key="template.id"
|
||||
:label="template.name"
|
||||
:value="template.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模板预览">
|
||||
<pre class="template-preview">{{ templatePreview }}</pre>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="useTemplate"
|
||||
:disabled="!form.selectedTemplate"
|
||||
>
|
||||
使用此模板
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 手动输入选项 -->
|
||||
<div v-if="form.method === 'manual'">
|
||||
<el-form-item label="输入类型">
|
||||
<el-radio-group v-model="form.manualType">
|
||||
<el-radio label="json">JSON数据</el-radio>
|
||||
<el-radio label="text">纯文本</el-radio>
|
||||
<el-radio label="file">文件上传</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="输入数据" v-if="form.manualType !== 'file'">
|
||||
<el-input
|
||||
v-if="form.manualType === 'text'"
|
||||
v-model="form.manualData"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入文本数据"
|
||||
/>
|
||||
<json-editor
|
||||
v-else-if="form.manualType === 'json'"
|
||||
v-model="form.jsonData"
|
||||
:height="200"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="上传文件" v-if="form.manualType === 'file'">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
drag
|
||||
:action="uploadUrl"
|
||||
:on-success="handleFileUploadSuccess"
|
||||
:on-error="handleFileUploadError"
|
||||
:headers="authHeaders"
|
||||
multiple
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽文件到此处或<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 生成结果预览 -->
|
||||
<div v-if="generatedData" class="result-section">
|
||||
<h3>生成的数据预览</h3>
|
||||
<div class="result-preview">
|
||||
<json-editor
|
||||
v-if="isJsonData(generatedData)"
|
||||
:value="generatedData"
|
||||
:height="200"
|
||||
:readonly="true"
|
||||
/>
|
||||
<pre v-else-if="typeof generatedData === 'string'" class="text-result">
|
||||
{{ generatedData }}
|
||||
</pre>
|
||||
<div v-else class="other-result">
|
||||
{{ JSON.stringify(generatedData, null, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-actions">
|
||||
<el-button @click="copyToClipboard">复制到剪贴板</el-button>
|
||||
<el-button type="primary" @click="emitGeneratedData">使用此数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import JsonEditor from './common/JsonEditor.vue' // 假设我们有一个JSON编辑器组件
|
||||
import { useAlgorithmStore } from '../stores/algorithm'
|
||||
|
||||
// 定义组件属性和事件
|
||||
const emit = defineEmits(['data-generated'])
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const generatedData = ref<any>(null)
|
||||
const templatePreview = ref('')
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
method: 'openai', // 'openai', 'template', 'manual'
|
||||
prompt: '',
|
||||
dataType: 'text',
|
||||
selectedTemplate: '',
|
||||
manualType: 'json',
|
||||
manualData: '',
|
||||
jsonData: {}
|
||||
})
|
||||
|
||||
// 模拟模板数据
|
||||
const templates = ref([
|
||||
{
|
||||
id: 'image-classification',
|
||||
name: '图像分类输入',
|
||||
description: '适用于图像分类算法的标准输入格式',
|
||||
data: {
|
||||
image_url: 'https://example.com/sample.jpg',
|
||||
preprocessing: {
|
||||
resize: [224, 224],
|
||||
normalize: [0.485, 0.456, 0.406, 0.229, 0.224, 0.225]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'text-classification',
|
||||
name: '文本分类输入',
|
||||
description: '适用于文本分类算法的标准输入格式',
|
||||
data: {
|
||||
text: '这是一个示例文本,用于文本分类任务',
|
||||
language: 'zh',
|
||||
max_length: 512
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'tabular-data',
|
||||
name: '表格数据输入',
|
||||
description: '适用于机器学习模型的表格数据输入',
|
||||
data: {
|
||||
features: ['age', 'income', 'education'],
|
||||
values: [25, 50000, 'bachelor'],
|
||||
target_column: 'prediction'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const uploadUrl = computed(() => {
|
||||
// 返回文件上传的API端点
|
||||
return '/api/data/media/upload'
|
||||
})
|
||||
|
||||
const authHeaders = computed(() => {
|
||||
// 返回认证头部
|
||||
const token = localStorage.getItem('token')
|
||||
return {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const generateWithOpenAI = async () => {
|
||||
if (!form.prompt) {
|
||||
ElMessage.warning('请输入描述提示')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
const result = await algorithmStore.generateSimulationData(form.prompt, form.dataType)
|
||||
|
||||
if (result) {
|
||||
generatedData.value = result.data
|
||||
ElMessage.success('仿真数据生成成功!')
|
||||
} else {
|
||||
ElMessage.error('生成仿真数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating simulation data:', error)
|
||||
ElMessage.error('生成仿真数据时发生错误')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadTemplateData = (templateId: string) => {
|
||||
const template = templates.value.find(t => t.id === templateId)
|
||||
if (template) {
|
||||
templatePreview.value = JSON.stringify(template.data, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
const useTemplate = () => {
|
||||
const template = templates.value.find(t => t.id === form.selectedTemplate)
|
||||
if (template) {
|
||||
generatedData.value = template.data
|
||||
ElMessage.success('模板数据已加载')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMethodChange = () => {
|
||||
// 重置相关字段
|
||||
if (form.method !== 'template') {
|
||||
form.selectedTemplate = ''
|
||||
templatePreview.value = ''
|
||||
}
|
||||
if (form.method !== 'manual') {
|
||||
form.manualData = ''
|
||||
form.jsonData = {}
|
||||
}
|
||||
}
|
||||
|
||||
const isJsonData = (data: any) => {
|
||||
try {
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
return true
|
||||
}
|
||||
JSON.parse(typeof data === 'string' ? data : JSON.stringify(data))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = () => {
|
||||
if (!generatedData.value) return
|
||||
|
||||
const textToCopy = typeof generatedData.value === 'string'
|
||||
? generatedData.value
|
||||
: JSON.stringify(generatedData.value, null, 2)
|
||||
|
||||
navigator.clipboard.writeText(textToCopy)
|
||||
.then(() => {
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err)
|
||||
ElMessage.error('复制失败')
|
||||
})
|
||||
}
|
||||
|
||||
const emitGeneratedData = () => {
|
||||
if (!generatedData.value) {
|
||||
ElMessage.warning('没有可使用的数据')
|
||||
return
|
||||
}
|
||||
|
||||
emit('data-generated', generatedData.value)
|
||||
ElMessage.success('数据已传递给父组件')
|
||||
}
|
||||
|
||||
const handleFileUploadSuccess = (response: any) => {
|
||||
console.log('File upload success:', response)
|
||||
generatedData.value = response
|
||||
ElMessage.success('文件上传成功')
|
||||
}
|
||||
|
||||
const handleFileUploadError = (error: any) => {
|
||||
console.error('File upload error:', error)
|
||||
ElMessage.error('文件上传失败')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 如果有默认方法,可以在这里设置
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.simulation-input-generator {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.template-preview {
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.result-preview {
|
||||
margin: 10px 0;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-result {
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.other-result {
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.json-editor-container) {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</file_path>
|
||||
145
frontend/src/components/common/JsonEditor.vue
Normal file
145
frontend/src/components/common/JsonEditor.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="json-editor" :style="{ height: height + 'px' }">
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="editorValue"
|
||||
:readonly="readonly"
|
||||
class="json-textarea"
|
||||
@input="handleInput"
|
||||
@blur="validateJson"
|
||||
></textarea>
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
|
||||
// 定义props
|
||||
interface Props {
|
||||
modelValue?: any
|
||||
value?: any
|
||||
height?: number
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300,
|
||||
readonly: false
|
||||
})
|
||||
|
||||
// 定义emit
|
||||
const emit = defineEmits(['update:modelValue', 'input', 'change'])
|
||||
|
||||
// 内部状态
|
||||
const textareaRef = ref<HTMLTextAreaElement>()
|
||||
const editorValue = ref('')
|
||||
const error = ref('')
|
||||
|
||||
// 将值转换为JSON字符串
|
||||
const formatJsonValue = (value: any): string => {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2) || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 从JSON字符串解析值
|
||||
const parseJsonValue = (str: string): any => {
|
||||
if (!str.trim()) return null
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化值
|
||||
const initializeValue = () => {
|
||||
const val = props.modelValue !== undefined ? props.modelValue : props.value
|
||||
editorValue.value = formatJsonValue(val)
|
||||
}
|
||||
|
||||
// 处理输入变化
|
||||
const handleInput = () => {
|
||||
try {
|
||||
const parsedValue = parseJsonValue(editorValue.value)
|
||||
error.value = ''
|
||||
emit('update:modelValue', parsedValue)
|
||||
emit('input', parsedValue)
|
||||
emit('change', parsedValue)
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message
|
||||
}
|
||||
}
|
||||
|
||||
// 验证JSON
|
||||
const validateJson = () => {
|
||||
if (!editorValue.value.trim()) {
|
||||
error.value = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
JSON.parse(editorValue.value)
|
||||
error.value = ''
|
||||
} catch (e) {
|
||||
error.value = 'Invalid JSON format'
|
||||
}
|
||||
}
|
||||
|
||||
// 监听外部值的变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
const formatted = formatJsonValue(newVal)
|
||||
if (formatted !== editorValue.value) {
|
||||
editorValue.value = formatted
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
initializeValue()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.json-editor {
|
||||
position: relative;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.json-textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
box-sizing: border-box;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.json-textarea:read-only {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 5px 10px;
|
||||
background-color: #fef0f0;
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid #f56c6c;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</file_path>
|
||||
551
frontend/src/components/core/AlgorithmCallAdvanced.vue
Normal file
551
frontend/src/components/core/AlgorithmCallAdvanced.vue
Normal file
@@ -0,0 +1,551 @@
|
||||
<template>
|
||||
<div class="algorithm-call-advanced">
|
||||
<!-- 算法信息 -->
|
||||
<el-card class="algorithm-info-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>{{ algorithm?.name || 'N/A' }}</h2>
|
||||
<el-tag :type="getStatusType(algorithm?.status || '')">
|
||||
{{ algorithm?.status || 'N/A' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p class="algorithm-description">{{ algorithm?.description }}</p>
|
||||
|
||||
<div class="algorithm-meta">
|
||||
<el-descriptions :column="2" size="small" border>
|
||||
<el-descriptions-item label="类型">{{ algorithm?.type || 'N/A' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="版本数量">{{ algorithm?.versions?.length || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDate(algorithm?.created_at || '') }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ formatDate(algorithm?.updated_at || '') }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 版本选择和参数配置 -->
|
||||
<el-card class="configuration-card">
|
||||
<template #header>
|
||||
<h3>配置选项</h3>
|
||||
</template>
|
||||
|
||||
<el-form :model="config" label-width="120px">
|
||||
<el-form-item label="算法版本">
|
||||
<el-select
|
||||
v-model="config.versionId"
|
||||
placeholder="请选择算法版本"
|
||||
@change="onVersionChange"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="version in algorithm?.versions || []"
|
||||
:key="version.id"
|
||||
:label="`${version.version}${version.is_default ? ' (默认)' : ''}`"
|
||||
:value="version.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="调用参数">
|
||||
<div class="params-container">
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
v-for="(param, key) in currentVersion?.params || {}"
|
||||
:key="key"
|
||||
:span="12"
|
||||
>
|
||||
<el-form-item :label="key">
|
||||
<component
|
||||
:is="getParamInputComponent(param)"
|
||||
v-model="config.params[key]"
|
||||
v-bind="getParamInputProps(param)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 输入数据区域 -->
|
||||
<el-card class="input-data-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>输入数据</h3>
|
||||
<el-button @click="showSimulator = true">
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
启动仿真输入生成器
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="inputTab" type="card">
|
||||
<el-tab-pane label="文本输入" name="text">
|
||||
<el-input
|
||||
v-model="inputData.text"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入文本数据"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="文件上传" name="files">
|
||||
<el-upload
|
||||
v-model:file-list="fileList"
|
||||
class="upload-demo"
|
||||
action="/api/data/media/upload"
|
||||
:headers="authHeaders"
|
||||
:before-upload="beforeFileUpload"
|
||||
:on-success="onFileUploadSuccess"
|
||||
:on-error="onFileUploadError"
|
||||
multiple
|
||||
>
|
||||
<el-button type="primary">选择文件</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持图片、文档等文件类型
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="结构化数据" name="structured">
|
||||
<json-editor
|
||||
v-model="inputData.structured"
|
||||
:height="300"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<!-- 调用控制 -->
|
||||
<div class="call-controls">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="isCalling"
|
||||
@click="callAlgorithm"
|
||||
:disabled="!canCall"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
执行算法
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
size="large"
|
||||
@click="clearResults"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
清空结果
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 执行结果 -->
|
||||
<el-card v-if="result" class="result-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>执行结果</h3>
|
||||
<el-tag :type="getResultStatusType(result.status)">
|
||||
{{ result.status }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="result-content">
|
||||
<div class="result-metadata">
|
||||
<el-descriptions :column="3" size="small">
|
||||
<el-descriptions-item label="执行时间">
|
||||
{{ result.response_time ? `${result.response_time}s` : 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行时间">
|
||||
{{ formatDate(result.created_at) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="调用ID">
|
||||
{{ result.id }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div v-if="result.status === 'success'" class="success-result">
|
||||
<h4>输出数据</h4>
|
||||
<json-editor
|
||||
:value="result.output_data"
|
||||
:height="400"
|
||||
:readonly="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="error-result">
|
||||
<el-alert
|
||||
:title="result.status"
|
||||
:description="result.error_message || '未知错误'"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 仿真输入生成器对话框 -->
|
||||
<el-dialog
|
||||
v-model="showSimulator"
|
||||
title="仿真输入生成器"
|
||||
width="80%"
|
||||
top="5vh"
|
||||
>
|
||||
<simulation-input-generator @data-generated="onSimulatedDataGenerated" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAlgorithmStore } from '../../stores/algorithm'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
MagicStick,
|
||||
VideoPlay,
|
||||
Delete
|
||||
} from '@element-plus/icons-vue'
|
||||
import SimulationInputGenerator from '../SimulationInputGenerator.vue'
|
||||
import JsonEditor from '../common/JsonEditor.vue'
|
||||
|
||||
// 类型定义
|
||||
interface Algorithm {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
status: string
|
||||
versions: AlgorithmVersion[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface AlgorithmVersion {
|
||||
id: string
|
||||
algorithm_id: string
|
||||
version: string
|
||||
url: string
|
||||
params: Record<string, any>
|
||||
input_schema: any
|
||||
output_schema: any
|
||||
is_default: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 定义props和emit
|
||||
interface Props {
|
||||
algorithmId?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 状态
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
|
||||
const config = reactive<{versionId: string, params: Record<string, any>}>({
|
||||
versionId: '',
|
||||
params: {}
|
||||
})
|
||||
|
||||
const inputData = reactive({
|
||||
text: '',
|
||||
structured: {},
|
||||
files: []
|
||||
})
|
||||
|
||||
const result = ref<any>(null)
|
||||
const isCalling = ref(false)
|
||||
const showSimulator = ref(false)
|
||||
const inputTab = ref('text')
|
||||
const fileList = ref<any[]>([])
|
||||
|
||||
// 计算属性
|
||||
const algorithm = computed<Algorithm | null>(() => algorithmStore.currentAlgorithm)
|
||||
const currentVersion = computed(() => {
|
||||
return algorithm.value?.versions?.find(v => v.id === config.versionId)
|
||||
})
|
||||
|
||||
const canCall = computed(() => {
|
||||
return config.versionId &&
|
||||
(inputData.text || Object.keys(inputData.structured).length > 0 || fileList.value.length > 0)
|
||||
})
|
||||
|
||||
const authHeaders = computed(() => {
|
||||
return {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadAlgorithm = async (id: string) => {
|
||||
try {
|
||||
await algorithmStore.fetchAlgorithmDetail(id)
|
||||
|
||||
// 选择默认版本
|
||||
if (algorithm.value?.versions && algorithm.value.versions.length > 0) {
|
||||
const defaultVersion = algorithm.value.versions.find(v => v.is_default)
|
||||
config.versionId = defaultVersion?.id || algorithm.value.versions[0].id
|
||||
|
||||
// 初始化参数
|
||||
onVersionChange()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load algorithm:', error)
|
||||
ElMessage.error('加载算法信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
const onVersionChange = () => {
|
||||
if (currentVersion.value?.params) {
|
||||
// 初始化参数值
|
||||
Object.keys(currentVersion.value.params).forEach(key => {
|
||||
const param = currentVersion.value!.params[key]
|
||||
config.params[key] = param.default || getDefaultParamValue(param)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getDefaultParamValue = (param: any) => {
|
||||
if (param.type === 'number') return param.default || 0
|
||||
if (param.type === 'boolean') return param.default || false
|
||||
if (param.options && param.options.length > 0) return param.options[0]
|
||||
return param.default || ''
|
||||
}
|
||||
|
||||
const getParamInputComponent = (param: any) => {
|
||||
if (param.type === 'boolean') return 'el-switch'
|
||||
if (param.options && param.options.length > 0) return 'el-select'
|
||||
if (param.type === 'number') return 'el-input-number'
|
||||
return 'el-input'
|
||||
}
|
||||
|
||||
const getParamInputProps = (param: any) => {
|
||||
if (param.type === 'boolean') return {}
|
||||
if (param.type === 'number') {
|
||||
return {
|
||||
min: param.min,
|
||||
max: param.max,
|
||||
step: param.step || 1
|
||||
}
|
||||
}
|
||||
if (param.options) {
|
||||
return {
|
||||
options: param.options.map((opt: any) => ({ label: opt, value: opt }))
|
||||
}
|
||||
}
|
||||
return {
|
||||
placeholder: param.description || `请输入${param.name || '参数'}`
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return 'success'
|
||||
case 'inactive': return 'info'
|
||||
case 'deprecated': return 'warning'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const getResultStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return 'success'
|
||||
case 'failed': return 'error'
|
||||
case 'running': return 'warning'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return 'N/A'
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
const callAlgorithm = async () => {
|
||||
if (!canCall.value) {
|
||||
ElMessage.warning('请填写必要参数和输入数据')
|
||||
return
|
||||
}
|
||||
|
||||
isCalling.value = true
|
||||
|
||||
try {
|
||||
// 准备输入数据
|
||||
let preparedInputData: any = {}
|
||||
|
||||
if (inputData.text) {
|
||||
preparedInputData.text = inputData.text
|
||||
}
|
||||
|
||||
if (Object.keys(inputData.structured).length > 0) {
|
||||
preparedInputData.structured = inputData.structured
|
||||
}
|
||||
|
||||
if (fileList.value.length > 0) {
|
||||
preparedInputData.files = fileList.value.map(file => ({
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
size: file.size
|
||||
}))
|
||||
}
|
||||
|
||||
// 调用算法
|
||||
const callResult = await algorithmStore.callAlgorithm({
|
||||
algorithm_id: algorithm.value!.id,
|
||||
version_id: config.versionId,
|
||||
input_data: preparedInputData,
|
||||
params: config.params
|
||||
})
|
||||
|
||||
if (callResult) {
|
||||
result.value = algorithmStore.callResult
|
||||
ElMessage.success('算法执行成功!')
|
||||
} else {
|
||||
ElMessage.error('算法执行失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Algorithm call error:', error)
|
||||
ElMessage.error('算法执行过程中发生错误')
|
||||
} finally {
|
||||
isCalling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearResults = () => {
|
||||
result.value = null
|
||||
ElMessage.info('结果已清空')
|
||||
}
|
||||
|
||||
const beforeFileUpload = (file: File) => {
|
||||
// 文件类型和大小验证
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'text/plain']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
ElMessage.error('不支持的文件类型')
|
||||
return false
|
||||
}
|
||||
|
||||
if (file.size > 10 * 1024 * 1024) { // 10MB
|
||||
ElMessage.error('文件大小不能超过10MB')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const onFileUploadSuccess = (response: any) => {
|
||||
ElMessage.success('文件上传成功')
|
||||
}
|
||||
|
||||
const onFileUploadError = (error: any) => {
|
||||
console.error('File upload error:', error)
|
||||
ElMessage.error('文件上传失败')
|
||||
}
|
||||
|
||||
const onSimulatedDataGenerated = (data: any) => {
|
||||
// 将生成的数据应用到输入区域
|
||||
if (typeof data === 'string') {
|
||||
inputTab.value = 'text'
|
||||
inputData.text = data
|
||||
} else if (typeof data === 'object') {
|
||||
inputTab.value = 'structured'
|
||||
inputData.structured = data
|
||||
}
|
||||
|
||||
showSimulator.value = false
|
||||
ElMessage.success('仿真数据已应用')
|
||||
}
|
||||
|
||||
// 监听props变化
|
||||
watch(() => props.algorithmId, (newId) => {
|
||||
if (newId) {
|
||||
loadAlgorithm(newId)
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (props.algorithmId) {
|
||||
loadAlgorithm(props.algorithmId)
|
||||
} else if (route.params.id) {
|
||||
loadAlgorithm(route.params.id as string)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.algorithm-call-advanced {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.configuration-card,
|
||||
.input-data-card,
|
||||
.result-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.params-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.call-controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.result-metadata {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.success-result,
|
||||
.error-result {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.success-result :deep(.json-editor) {
|
||||
border: 1px solid #e1f3d8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.error-result {
|
||||
padding: 15px;
|
||||
background-color: #fef0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__body) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</file_path>
|
||||
662
frontend/src/components/core/HistoryManager.vue
Normal file
662
frontend/src/components/core/HistoryManager.vue
Normal file
@@ -0,0 +1,662 @@
|
||||
<template>
|
||||
<div class="history-manager">
|
||||
<el-card class="history-control-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>历史记录管理</h3>
|
||||
<div class="control-buttons">
|
||||
<el-button @click="refreshHistory">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button type="primary" @click="exportHistory">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出历史
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="filters" inline class="filter-form">
|
||||
<el-form-item label="算法">
|
||||
<el-select
|
||||
v-model="filters.algorithmId"
|
||||
placeholder="选择算法"
|
||||
clearable
|
||||
@change="applyFilters"
|
||||
>
|
||||
<el-option
|
||||
v-for="algorithm in availableAlgorithms"
|
||||
:key="algorithm.id"
|
||||
:label="algorithm.name"
|
||||
:value="algorithm.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="filters.status"
|
||||
placeholder="选择状态"
|
||||
clearable
|
||||
@change="applyFilters"
|
||||
>
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="failed" />
|
||||
<el-option label="待处理" value="pending" />
|
||||
<el-option label="运行中" value="running" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
@change="applyFilters"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.totalCalls }}</div>
|
||||
<div class="stat-label">总调用次数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.successRate }}%</div>
|
||||
<div class="stat-label">成功率</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.avgResponseTime }}s</div>
|
||||
<div class="stat-label">平均响应时间</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.todayCalls }}</div>
|
||||
<div class="stat-label">今日调用</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 历史记录表格 -->
|
||||
<el-card class="history-table-card">
|
||||
<template #header>
|
||||
<div class="table-header">
|
||||
<h4>调用历史</h4>
|
||||
<div class="table-controls">
|
||||
<el-checkbox v-model="showDetails" @change="toggleDetails">显示详情</el-checkbox>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="compareSelected"
|
||||
>
|
||||
对比选中项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
:data="historyData"
|
||||
@selection-change="handleSelectionChange"
|
||||
row-key="id"
|
||||
:default-expand-all="showDetails"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="调用ID" width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="algorithm_name" label="算法名称" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="version" label="版本" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="response_time" label="响应时间(s)" width="120" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ row.response_time ? row.response_time.toFixed(3) : 'N/A' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="调用时间" width="180" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewDetails(row)">查看详情</el-button>
|
||||
<el-button size="small" type="primary" @click="rerunCall(row)">重新运行</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 详情展开行 -->
|
||||
<el-table-column v-if="showDetails" type="expand" width="1">
|
||||
<template #default="{ row }">
|
||||
<div class="expand-details">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="输入数据">
|
||||
<pre class="data-preview">{{ JSON.stringify(row.input_data, null, 2) }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="输出数据">
|
||||
<pre class="data-preview">{{ JSON.stringify(row.output_data, null, 2) }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数">
|
||||
<pre class="data-preview">{{ JSON.stringify(row.params, null, 2) }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="错误信息" v-if="row.error_message">
|
||||
<span class="error-text">{{ row.error_message }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="pagination.currentPage"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pagination.pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="调用详情"
|
||||
width="80%"
|
||||
top="5vh"
|
||||
>
|
||||
<div v-if="selectedRow" class="detail-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="调用ID">{{ selectedRow.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="算法名称">{{ selectedRow.algorithm_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="算法ID">{{ selectedRow.algorithm_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="版本">{{ selectedRow.version }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusType(selectedRow.status)">{{ selectedRow.status }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="响应时间">
|
||||
{{ selectedRow.response_time ? selectedRow.response_time.toFixed(3) + 's' : 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="调用时间">{{ formatDate(selectedRow.created_at) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">输入数据</el-divider>
|
||||
<json-editor :value="selectedRow.input_data" :height="200" :readonly="true" />
|
||||
|
||||
<el-divider content-position="left">输出数据</el-divider>
|
||||
<json-editor :value="selectedRow.output_data" :height="300" :readonly="true" />
|
||||
|
||||
<el-divider content-position="left">参数</el-divider>
|
||||
<json-editor :value="selectedRow.params" :height="150" :readonly="true" />
|
||||
|
||||
<el-divider content-position="left">错误信息</el-divider>
|
||||
<el-alert
|
||||
v-if="selectedRow.error_message"
|
||||
:title="selectedRow.error_message"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert v-else title="无错误信息" type="success" show-icon />
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 对比对话框 -->
|
||||
<el-dialog
|
||||
v-model="compareDialogVisible"
|
||||
title="结果对比"
|
||||
width="90%"
|
||||
top="5vh"
|
||||
>
|
||||
<div v-if="comparisonData.length > 0" class="comparison-content">
|
||||
<el-table :data="comparisonData" style="width: 100%">
|
||||
<el-table-column prop="algorithm_name" label="算法名称" width="150" />
|
||||
<el-table-column prop="version" label="版本" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="response_time" label="响应时间(s)" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.response_time ? row.response_time.toFixed(3) : 'N/A' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="调用时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewCompareDetails(row)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-divider content-position="left">对比结果可视化</el-divider>
|
||||
<result-visualization
|
||||
:result-data="combinedComparisonData"
|
||||
:enable-comparison="true"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { Refresh, Download } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import JsonEditor from '../common/JsonEditor.vue'
|
||||
import ResultVisualization from './ResultVisualization.vue'
|
||||
import { useAlgorithmStore } from '../../stores/algorithm'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
|
||||
// 定义props
|
||||
interface Props {
|
||||
userId?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
userId: ''
|
||||
})
|
||||
|
||||
// 状态
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
const userStore = useUserStore()
|
||||
const historyData = ref<any[]>([])
|
||||
const availableAlgorithms = ref<any[]>([])
|
||||
const selectedRow = ref<any>(null)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const detailDialogVisible = ref(false)
|
||||
const compareDialogVisible = ref(false)
|
||||
const comparisonData = ref<any[]>([])
|
||||
const showDetails = ref(false)
|
||||
|
||||
// 过滤器
|
||||
const filters = reactive({
|
||||
algorithmId: '',
|
||||
status: '',
|
||||
dateRange: []
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 统计信息
|
||||
const stats = reactive({
|
||||
totalCalls: 0,
|
||||
successRate: 0,
|
||||
avgResponseTime: 0,
|
||||
todayCalls: 0
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const combinedComparisonData = computed(() => {
|
||||
// 将多个结果合并为一个可用于可视化的数据结构
|
||||
return {
|
||||
results: comparisonData.value.map((item, index) => ({
|
||||
id: item.id,
|
||||
algorithm: item.algorithm_name,
|
||||
version: item.version,
|
||||
status: item.status,
|
||||
response_time: item.response_time,
|
||||
created_at: item.created_at
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
// 加载算法列表
|
||||
await algorithmStore.fetchAlgorithms()
|
||||
availableAlgorithms.value = algorithmStore.algorithms
|
||||
|
||||
// 加载历史记录
|
||||
await loadCallHistory()
|
||||
|
||||
// 加载统计信息
|
||||
await loadStatistics()
|
||||
} catch (error) {
|
||||
console.error('Failed to load history:', error)
|
||||
ElMessage.error('加载历史记录失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadCallHistory = async () => {
|
||||
try {
|
||||
// 模拟API调用 - 在实际应用中,这里会调用后端API
|
||||
// 获取当前用户ID
|
||||
const userId = props.userId || userStore.user?.id
|
||||
|
||||
// 构造查询参数
|
||||
const params: any = {
|
||||
user_id: userId,
|
||||
skip: (pagination.currentPage - 1) * pagination.pageSize,
|
||||
limit: pagination.pageSize
|
||||
}
|
||||
|
||||
if (filters.algorithmId) {
|
||||
params.algorithm_id = filters.algorithmId
|
||||
}
|
||||
|
||||
if (filters.status) {
|
||||
params.status = filters.status
|
||||
}
|
||||
|
||||
// 模拟加载数据
|
||||
historyData.value = [
|
||||
{
|
||||
id: 'call-001',
|
||||
algorithm_id: 'alg-001',
|
||||
algorithm_name: '图像分类算法',
|
||||
version: 'v1.0',
|
||||
status: 'success',
|
||||
response_time: 1.234,
|
||||
created_at: '2023-12-01T10:30:00Z',
|
||||
input_data: { image_url: 'https://example.com/image.jpg' },
|
||||
output_data: { predictions: [{ class: 'cat', confidence: 0.95 }] },
|
||||
params: { threshold: 0.5 }
|
||||
},
|
||||
{
|
||||
id: 'call-002',
|
||||
algorithm_id: 'alg-002',
|
||||
algorithm_name: '文本情感分析',
|
||||
version: 'v2.1',
|
||||
status: 'success',
|
||||
response_time: 0.789,
|
||||
created_at: '2023-12-01T11:15:00Z',
|
||||
input_data: { text: '今天天气很好' },
|
||||
output_data: { sentiment: 'positive', score: 0.89 },
|
||||
params: { model: 'bert-base' }
|
||||
},
|
||||
{
|
||||
id: 'call-003',
|
||||
algorithm_id: 'alg-001',
|
||||
algorithm_name: '图像分类算法',
|
||||
version: 'v1.0',
|
||||
status: 'failed',
|
||||
response_time: null,
|
||||
created_at: '2023-12-01T12:00:00Z',
|
||||
input_data: { image_url: 'https://example.com/bad_image.jpg' },
|
||||
output_data: {},
|
||||
params: { threshold: 0.9 },
|
||||
error_message: '模型推理失败'
|
||||
}
|
||||
]
|
||||
|
||||
pagination.total = historyData.value.length
|
||||
} catch (error) {
|
||||
console.error('Failed to load call history:', error)
|
||||
ElMessage.error('加载调用历史失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadStatistics = async () => {
|
||||
try {
|
||||
// 模拟加载统计数据
|
||||
stats.totalCalls = 156
|
||||
stats.successRate = 92.3
|
||||
stats.avgResponseTime = 1.45
|
||||
stats.todayCalls = 12
|
||||
} catch (error) {
|
||||
console.error('Failed to load statistics:', error)
|
||||
ElMessage.error('加载统计信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
const refreshHistory = async () => {
|
||||
await loadHistory()
|
||||
ElMessage.success('历史记录已刷新')
|
||||
}
|
||||
|
||||
const exportHistory = async () => {
|
||||
try {
|
||||
// 模拟导出功能
|
||||
ElMessage.success('历史记录导出功能已启动')
|
||||
} catch (error) {
|
||||
console.error('Failed to export history:', error)
|
||||
ElMessage.error('导出历史记录失败')
|
||||
}
|
||||
}
|
||||
|
||||
const applyFilters = async () => {
|
||||
await loadCallHistory()
|
||||
}
|
||||
|
||||
const resetFilters = () => {
|
||||
filters.algorithmId = ''
|
||||
filters.status = ''
|
||||
filters.dateRange = []
|
||||
pagination.currentPage = 1
|
||||
loadCallHistory()
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
const viewDetails = (row: any) => {
|
||||
selectedRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const viewCompareDetails = (row: any) => {
|
||||
selectedRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return 'success'
|
||||
case 'failed': return 'danger'
|
||||
case 'pending': return 'warning'
|
||||
case 'running': return 'info'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return 'N/A'
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
const toggleDetails = () => {
|
||||
// 切换详情显示状态
|
||||
}
|
||||
|
||||
const compareSelected = async () => {
|
||||
if (selectedRows.value.length < 2) {
|
||||
ElMessage.warning('请至少选择两个调用记录进行对比')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 模拟获取对比数据
|
||||
const callIds = selectedRows.value.map((row: any) => row.id)
|
||||
comparisonData.value = [...selectedRows.value]
|
||||
compareDialogVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('Failed to get comparison data:', error)
|
||||
ElMessage.error('获取对比数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const rerunCall = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要重新运行调用 "${row.id}" 吗?`,
|
||||
'重新运行',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 模拟重新运行调用
|
||||
ElMessage.success('重新运行请求已提交')
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.pageSize = val
|
||||
loadCallHistory()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.currentPage = val
|
||||
loadCallHistory()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadHistory()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.history-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.history-control-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.history-table-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.expand-details {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f5f5;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #F56C6C;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.comparison-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__body) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
:deep(.el-table__expanded-cell) {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</file_path>
|
||||
552
frontend/src/components/core/ResultVisualization.vue
Normal file
552
frontend/src/components/core/ResultVisualization.vue
Normal file
@@ -0,0 +1,552 @@
|
||||
<template>
|
||||
<div class="result-visualization">
|
||||
<el-card class="visualization-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h3>结果可视化</h3>
|
||||
<div class="visualization-controls">
|
||||
<el-select
|
||||
v-model="visualizationType"
|
||||
placeholder="选择可视化类型"
|
||||
@change="renderVisualization"
|
||||
>
|
||||
<el-option label="图表" value="chart" />
|
||||
<el-option label="图像对比" value="image-comparison" />
|
||||
<el-option label="表格" value="table" />
|
||||
<el-option label="3D可视化" value="3d" />
|
||||
</el-select>
|
||||
|
||||
<el-button @click="exportResult">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出结果
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 图表可视化 -->
|
||||
<div v-if="visualizationType === 'chart'" class="chart-container">
|
||||
<div ref="chartContainer" class="chart-wrapper"></div>
|
||||
</div>
|
||||
|
||||
<!-- 图像对比 -->
|
||||
<div v-else-if="visualizationType === 'image-comparison'" class="image-comparison-container">
|
||||
<div class="comparison-grid">
|
||||
<div v-for="(item, index) in comparisonData" :key="index" class="comparison-item">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<img :src="item.imageUrl" :alt="item.title" class="comparison-image" />
|
||||
<p class="comparison-label">{{ item.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格可视化 -->
|
||||
<div v-else-if="visualizationType === 'table'" class="table-container">
|
||||
<el-table :data="tableData" style="width: 100%" border>
|
||||
<el-table-column
|
||||
v-for="column in tableColumns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:formatter="column.formatter"
|
||||
/>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 3D可视化 -->
|
||||
<div v-else-if="visualizationType === '3d'" class="three-d-container">
|
||||
<div ref="threeContainer" class="three-wrapper"></div>
|
||||
</div>
|
||||
|
||||
<!-- 通用可视化 -->
|
||||
<div v-else class="generic-container">
|
||||
<pre>{{ JSON.stringify(resultData, null, 2) }}</pre>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 效果对比面板 -->
|
||||
<el-card v-if="enableComparison" class="comparison-panel">
|
||||
<template #header>
|
||||
<h3>效果对比</h3>
|
||||
</template>
|
||||
|
||||
<div class="comparison-controls">
|
||||
<el-button @click="addComparisonItem">添加对比项</el-button>
|
||||
<el-button @click="compareResults">开始对比</el-button>
|
||||
</div>
|
||||
|
||||
<div class="comparison-results">
|
||||
<div v-for="(item, index) in comparisonItems" :key="index" class="comparison-result-item">
|
||||
<div class="result-header">
|
||||
<span class="result-title">{{ item.title }}</span>
|
||||
<el-button size="small" type="danger" @click="removeComparisonItem(index)">删除</el-button>
|
||||
</div>
|
||||
<div class="result-content">
|
||||
<div v-if="item.type === 'image'">
|
||||
<img :src="item.data" :alt="item.title" class="result-image" />
|
||||
</div>
|
||||
<div v-else-if="item.type === 'data'">
|
||||
<pre>{{ JSON.stringify(item.data, null, 2) }}</pre>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>结果类型: {{ item.type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Download } from '@element-plus/icons-vue'
|
||||
import * as echarts from 'echarts'
|
||||
import * as THREE from 'three'
|
||||
|
||||
// 定义props
|
||||
interface Props {
|
||||
resultData?: any
|
||||
enableComparison?: boolean
|
||||
algorithmType?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
resultData: () => ({}),
|
||||
enableComparison: false,
|
||||
algorithmType: 'general'
|
||||
})
|
||||
|
||||
// 引用
|
||||
const chartContainer = ref<HTMLDivElement>()
|
||||
const threeContainer = ref<HTMLDivElement>()
|
||||
const chartInstance = ref<echarts.ECharts | null>(null)
|
||||
const threeScene = ref<THREE.Scene | null>(null)
|
||||
const threeCamera = ref<THREE.PerspectiveCamera | null>(null)
|
||||
const threeRenderer = ref<THREE.WebGLRenderer | null>(null)
|
||||
|
||||
// 状态
|
||||
const visualizationType = ref('chart')
|
||||
const comparisonItems = ref<any[]>([])
|
||||
const comparisonData = ref<any[]>([])
|
||||
const tableData = ref<any[]>([])
|
||||
const tableColumns = ref<any[]>([])
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 根据结果数据类型自动选择可视化类型
|
||||
if (props.resultData) {
|
||||
detectVisualizationType(props.resultData)
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
initChart()
|
||||
init3D()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 销毁图表实例
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.dispose()
|
||||
}
|
||||
|
||||
// 销毁3D场景
|
||||
destroy3D()
|
||||
})
|
||||
|
||||
// 监听结果数据变化
|
||||
watch(() => props.resultData, (newData) => {
|
||||
if (newData) {
|
||||
detectVisualizationType(newData)
|
||||
renderVisualization()
|
||||
}
|
||||
})
|
||||
|
||||
// 方法
|
||||
const detectVisualizationType = (data: any) => {
|
||||
if (data && typeof data === 'object') {
|
||||
if (data.images || data.image_urls) {
|
||||
visualizationType.value = 'image-comparison'
|
||||
} else if (Array.isArray(data) && data.length > 0) {
|
||||
visualizationType.value = 'table'
|
||||
} else if (data.chart_data) {
|
||||
visualizationType.value = 'chart'
|
||||
} else {
|
||||
visualizationType.value = 'chart' // 默认图表
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initChart = () => {
|
||||
if (chartContainer.value) {
|
||||
chartInstance.value = echarts.init(chartContainer.value)
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.resize()
|
||||
}
|
||||
}
|
||||
|
||||
const renderVisualization = () => {
|
||||
switch (visualizationType.value) {
|
||||
case 'chart':
|
||||
renderChart()
|
||||
break
|
||||
case 'image-comparison':
|
||||
renderImageComparison()
|
||||
break
|
||||
case 'table':
|
||||
renderTable()
|
||||
break
|
||||
case '3d':
|
||||
render3D()
|
||||
break
|
||||
default:
|
||||
// 通用显示
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const renderChart = () => {
|
||||
if (!chartInstance.value) return
|
||||
|
||||
// 根据数据类型生成不同的图表配置
|
||||
const option = getChartOption(props.resultData)
|
||||
chartInstance.value.setOption(option)
|
||||
}
|
||||
|
||||
const getChartOption = (data: any) => {
|
||||
// 简化的图表配置,实际应用中需要根据数据类型动态生成
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
// 假设是柱状图数据
|
||||
return {
|
||||
title: {
|
||||
text: '结果可视化'
|
||||
},
|
||||
tooltip: {},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.map((item: any, index: number) => `项目${index + 1}`)
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
data: data.map((item: any) => typeof item === 'object' ? item.value || 0 : item),
|
||||
type: 'bar'
|
||||
}]
|
||||
}
|
||||
} else if (typeof data === 'object' && data.labels && data.datasets) {
|
||||
// 假设是饼图数据
|
||||
return {
|
||||
title: {
|
||||
text: '结果可视化'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left'
|
||||
},
|
||||
series: [{
|
||||
name: '结果',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: data.datasets,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
// 默认柱状图
|
||||
return {
|
||||
title: {
|
||||
text: '结果可视化'
|
||||
},
|
||||
tooltip: {},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['结果']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [{
|
||||
data: [JSON.stringify(data).length],
|
||||
type: 'bar'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const renderImageComparison = () => {
|
||||
// 处理图像对比数据
|
||||
if (props.resultData && props.resultData.images) {
|
||||
comparisonData.value = props.resultData.images.map((img: any, index: number) => ({
|
||||
title: `结果 ${index + 1}`,
|
||||
imageUrl: img.url || img,
|
||||
label: img.label || `图像 ${index + 1}`
|
||||
}))
|
||||
} else {
|
||||
comparisonData.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const renderTable = () => {
|
||||
// 处理表格数据
|
||||
if (Array.isArray(props.resultData)) {
|
||||
tableData.value = props.resultData
|
||||
// 生成列定义
|
||||
if (props.resultData.length > 0) {
|
||||
tableColumns.value = Object.keys(props.resultData[0]).map(key => ({
|
||||
prop: key,
|
||||
label: key,
|
||||
formatter: (row: any, column: any, cellValue: any) => {
|
||||
if (typeof cellValue === 'object') {
|
||||
return JSON.stringify(cellValue)
|
||||
}
|
||||
return cellValue
|
||||
}
|
||||
}))
|
||||
}
|
||||
} else if (typeof props.resultData === 'object') {
|
||||
// 如果是对象,转换为单行表格
|
||||
tableData.value = [props.resultData]
|
||||
tableColumns.value = Object.keys(props.resultData).map(key => ({
|
||||
prop: key,
|
||||
label: key
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const init3D = () => {
|
||||
if (threeContainer.value) {
|
||||
// 初始化Three.js场景
|
||||
threeScene.value = new THREE.Scene()
|
||||
threeCamera.value = new THREE.PerspectiveCamera(75, threeContainer.value.clientWidth / threeContainer.value.clientHeight, 0.1, 1000)
|
||||
threeRenderer.value = new THREE.WebGLRenderer({ antialias: true })
|
||||
|
||||
threeRenderer.value.setSize(threeContainer.value.clientWidth, threeContainer.value.clientHeight)
|
||||
threeContainer.value.appendChild(threeRenderer.value.domElement)
|
||||
|
||||
// 添加基本几何体
|
||||
const geometry = new THREE.BoxGeometry()
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
threeScene.value.add(cube)
|
||||
|
||||
threeCamera.value.position.z = 5
|
||||
|
||||
// 开始动画循环
|
||||
animate3D()
|
||||
}
|
||||
}
|
||||
|
||||
const animate3D = () => {
|
||||
if (threeScene.value && threeCamera.value && threeRenderer.value && threeContainer.value) {
|
||||
requestAnimationFrame(animate3D)
|
||||
|
||||
// 旋转立方体
|
||||
const objects = threeScene.value.children.filter(obj => obj instanceof THREE.Mesh)
|
||||
objects.forEach(obj => {
|
||||
obj.rotation.x += 0.01
|
||||
obj.rotation.y += 0.01
|
||||
})
|
||||
|
||||
threeRenderer.value.setSize(threeContainer.value.clientWidth, threeContainer.value.clientHeight)
|
||||
threeRenderer.value.render(threeScene.value, threeCamera.value)
|
||||
}
|
||||
}
|
||||
|
||||
const render3D = () => {
|
||||
// 3D渲染由动画循环处理
|
||||
}
|
||||
|
||||
const destroy3D = () => {
|
||||
if (threeRenderer.value && threeContainer.value) {
|
||||
threeContainer.value.removeChild(threeRenderer.value.domElement)
|
||||
}
|
||||
if (threeRenderer.value) {
|
||||
threeRenderer.value.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
const exportResult = () => {
|
||||
// 导出结果的实现
|
||||
const dataStr = JSON.stringify(props.resultData, null, 2)
|
||||
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr)
|
||||
|
||||
const exportFileDefaultName = 'result_export.json'
|
||||
|
||||
const linkElement = document.createElement('a')
|
||||
linkElement.setAttribute('href', dataUri)
|
||||
linkElement.setAttribute('download', exportFileDefaultName)
|
||||
linkElement.click()
|
||||
|
||||
ElMessage.success('结果已导出')
|
||||
}
|
||||
|
||||
const addComparisonItem = () => {
|
||||
// 添加对比项
|
||||
comparisonItems.value.push({
|
||||
title: `对比项 ${comparisonItems.value.length + 1}`,
|
||||
type: 'data',
|
||||
data: props.resultData
|
||||
})
|
||||
}
|
||||
|
||||
const removeComparisonItem = (index: number) => {
|
||||
comparisonItems.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const compareResults = () => {
|
||||
// 执行对比分析
|
||||
if (comparisonItems.value.length < 2) {
|
||||
ElMessage.warning('至少需要两个对比项')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success(`已对 ${comparisonItems.value.length} 个结果进行对比`)
|
||||
// 这里可以添加具体的对比逻辑
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-visualization {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.visualization-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.visualization-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-comparison-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.comparison-image {
|
||||
max-width: 100%;
|
||||
height: 200px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.comparison-label {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.three-d-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.three-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.generic-container {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.comparison-panel {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.comparison-controls {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.comparison-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.comparison-result-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.result-image {
|
||||
max-width: 100%;
|
||||
height: 150px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</file_path>
|
||||
98
frontend/src/main.ts
Normal file
98
frontend/src/main.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 配置 Monaco Editor 环境
|
||||
(window as any).MonacoEnvironment = {
|
||||
getWorker(_: any, label: string) {
|
||||
// 使用 Vite 特定的方式加载 web worker
|
||||
// 这种方式会确保 Vite 正确处理 web worker 文件
|
||||
// 添加 type: 'module' 选项,确保以模块模式加载 worker 文件
|
||||
if (label === 'json') {
|
||||
return new Worker(new URL('monaco-editor/esm/vs/language/json/json.worker.js', import.meta.url), { type: 'module' })
|
||||
} else if (label === 'css' || label === 'scss' || label === 'less') {
|
||||
return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url), { type: 'module' })
|
||||
} else if (label === 'html' || label === 'handlebars' || label === 'razor') {
|
||||
return new Worker(new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url), { type: 'module' })
|
||||
} else if (label === 'typescript' || label === 'javascript') {
|
||||
return new Worker(new URL('monaco-editor/esm/vs/language/typescript/ts.worker.js', import.meta.url), { type: 'module' })
|
||||
} else {
|
||||
return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url), { type: 'module' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 配置axios
|
||||
// 移除baseURL配置,使用Vite代理处理API路径映射
|
||||
axios.defaults.headers.common['Content-Type'] = 'application/json'
|
||||
axios.defaults.withCredentials = true
|
||||
|
||||
// 添加请求拦截器,用于添加认证token和调试日志
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
// 调试日志
|
||||
console.log('发送请求:', config.method?.toUpperCase(), config.url)
|
||||
console.log('完整URL:', (axios.defaults.baseURL || '') + (config.url || ''))
|
||||
|
||||
// 添加认证token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 添加响应拦截器,用于处理错误
|
||||
axios.interceptors.response.use(
|
||||
response => {
|
||||
console.log('收到响应:', response.status, response.config.url)
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
console.error('请求错误:', error)
|
||||
console.error('错误状态:', error.response?.status)
|
||||
console.error('错误数据:', error.response?.data)
|
||||
|
||||
// 处理401错误,跳转到登录页
|
||||
if (error.response && error.response.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
router.push('/login')
|
||||
} else if (error.response && error.response.status === 404) {
|
||||
// 404错误
|
||||
ElMessage.error('请求的接口不存在: ' + (error.config?.url || '未知路径'))
|
||||
} else if (error.response && error.response.data) {
|
||||
// 显示错误信息
|
||||
const errorMessage = error.response.data.detail || error.response.data.message || '请求失败'
|
||||
ElMessage.error(errorMessage)
|
||||
} else if (error.request) {
|
||||
// 请求已发出但没有收到响应
|
||||
ElMessage.error('网络错误,请检查网络连接')
|
||||
} else {
|
||||
// 其他错误
|
||||
ElMessage.error('请求失败,请稍后重试')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册插件
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
174
frontend/src/router/index.ts
Normal file
174
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
// 定义路由
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../views/HomeView.vue'),
|
||||
meta: {
|
||||
title: '首页'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/algorithms',
|
||||
name: 'Algorithms',
|
||||
component: () => import('../views/AlgorithmsView.vue'),
|
||||
meta: {
|
||||
title: '算法列表'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/algorithm/:id',
|
||||
name: 'AlgorithmDetail',
|
||||
component: () => import('../views/AlgorithmDetailView.vue'),
|
||||
meta: {
|
||||
title: '算法详情'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/algorithm/:id/call',
|
||||
name: 'AlgorithmCall',
|
||||
component: () => import('../views/AlgorithmCallView.vue'),
|
||||
meta: {
|
||||
title: '算法调用'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/algorithm/:id/versions',
|
||||
name: 'AlgorithmVersions',
|
||||
component: () => import('../views/AlgorithmVersionsView.vue'),
|
||||
meta: {
|
||||
title: '算法版本管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'Admin',
|
||||
component: () => import('../views/AdminView.vue'),
|
||||
meta: {
|
||||
title: '管理员中心',
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'algorithms',
|
||||
name: 'AdminAlgorithms',
|
||||
component: () => import('../views/admin/AdminAlgorithmsView.vue'),
|
||||
meta: {
|
||||
title: '算法仓库管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'services',
|
||||
name: 'AdminAlgorithmServices',
|
||||
component: () => import('../views/admin/AdminAlgorithmServicesView.vue'),
|
||||
meta: {
|
||||
title: '算法服务管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'AdminUsers',
|
||||
component: () => import('../views/admin/AdminUsersView.vue'),
|
||||
meta: {
|
||||
title: '用户管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'api-keys',
|
||||
name: 'AdminApiKeys',
|
||||
component: () => import('../views/admin/AdminApiKeysView.vue'),
|
||||
meta: {
|
||||
title: 'API密钥管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'api',
|
||||
name: 'AdminApiManagement',
|
||||
component: () => import('../views/admin/AdminApiManagementView.vue'),
|
||||
meta: {
|
||||
title: 'API管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'service-registration',
|
||||
name: 'AdminServiceRegistration',
|
||||
component: () => import('../views/admin/AdminServiceRegistrationView.vue'),
|
||||
meta: {
|
||||
title: '服务注册'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/LoginView.vue'),
|
||||
meta: {
|
||||
title: '登录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('../views/RegisterView.vue'),
|
||||
meta: {
|
||||
title: '注册'
|
||||
}
|
||||
},
|
||||
// 404页面
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../views/NotFoundView.vue'),
|
||||
meta: {
|
||||
title: '页面不存在'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 创建路由器
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫,用于设置页面标题和权限控制
|
||||
router.beforeEach((to, _from, next) => {
|
||||
// 设置页面标题
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - 智能算法展示平台`
|
||||
}
|
||||
|
||||
// 权限控制
|
||||
if (to.meta.requiresAuth) {
|
||||
const token = localStorage.getItem('token')
|
||||
const user = localStorage.getItem('user')
|
||||
|
||||
if (!token || !user) {
|
||||
next({ name: 'Login' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要管理员权限
|
||||
if (to.meta.requiresAdmin) {
|
||||
try {
|
||||
const userObj = JSON.parse(user)
|
||||
if (userObj.role !== 'admin') {
|
||||
next({ name: 'Home' })
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
next({ name: 'Login' })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
194
frontend/src/stores/algorithm.ts
Normal file
194
frontend/src/stores/algorithm.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
|
||||
// 定义算法类型
|
||||
interface Algorithm {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
status: string
|
||||
versions: AlgorithmVersion[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 定义算法版本类型
|
||||
interface AlgorithmVersion {
|
||||
id: string
|
||||
algorithm_id: string
|
||||
version: string
|
||||
url: string
|
||||
params: any
|
||||
input_schema: any
|
||||
output_schema: any
|
||||
is_default: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// 定义算法调用请求类型
|
||||
interface AlgorithmCallRequest {
|
||||
algorithm_id: string
|
||||
version_id: string
|
||||
input_data: any
|
||||
params: any
|
||||
}
|
||||
|
||||
// 定义算法调用结果类型
|
||||
interface AlgorithmCallResult {
|
||||
id: string
|
||||
status: string
|
||||
output_data: any
|
||||
response_time: number
|
||||
error_message: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 定义算法存储
|
||||
export const useAlgorithmStore = defineStore('algorithm', {
|
||||
state: () => ({
|
||||
algorithms: [] as Algorithm[],
|
||||
currentAlgorithm: null as Algorithm | null,
|
||||
currentVersion: null as AlgorithmVersion | null,
|
||||
callResult: null as AlgorithmCallResult | null,
|
||||
loading: false,
|
||||
error: null as string | null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getAlgorithmById: (state) => (id: string) => {
|
||||
return state.algorithms.find(algorithm => algorithm.id === id)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 获取算法列表
|
||||
async fetchAlgorithms(type?: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const params = type ? { type } : {}
|
||||
const response = await axios.get('/algorithms', { params })
|
||||
this.algorithms = response.data.algorithms
|
||||
return true
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '获取算法列表失败'
|
||||
return false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取算法详情
|
||||
async fetchAlgorithmDetail(id: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/${id}`)
|
||||
this.currentAlgorithm = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '获取算法详情失败'
|
||||
return false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取算法版本列表
|
||||
async fetchAlgorithmVersions(algorithmId: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/${algorithmId}/versions`)
|
||||
if (this.currentAlgorithm) {
|
||||
this.currentAlgorithm.versions = response.data
|
||||
}
|
||||
return true
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '获取算法版本列表失败'
|
||||
return false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 调用算法
|
||||
async callAlgorithm(request: AlgorithmCallRequest) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/algorithms/call', request)
|
||||
this.callResult = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '调用算法失败'
|
||||
return false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取算法调用结果
|
||||
async fetchCallResult(callId: string) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/algorithms/calls/${callId}`)
|
||||
this.callResult = response.data
|
||||
return true
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '获取调用结果失败'
|
||||
return false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 生成仿真输入数据
|
||||
async generateSimulationData(prompt: string, dataType: string = 'text') {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/openai/generate-data', { prompt, data_type: dataType })
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '生成仿真数据失败'
|
||||
return null
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 创建算法
|
||||
async createAlgorithm(algorithmData: any) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.post('/algorithms', algorithmData)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '创建算法失败'
|
||||
return null
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 重置状态
|
||||
reset() {
|
||||
this.currentAlgorithm = null
|
||||
this.currentVersion = null
|
||||
this.callResult = null
|
||||
this.error = null
|
||||
}
|
||||
}
|
||||
})
|
||||
153
frontend/src/stores/user.ts
Normal file
153
frontend/src/stores/user.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
|
||||
// 定义用户类型
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
role: string
|
||||
status: string
|
||||
}
|
||||
|
||||
// 定义登录请求类型
|
||||
interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
// 定义注册请求类型
|
||||
interface RegisterRequest {
|
||||
username: string
|
||||
password: string
|
||||
email: string
|
||||
role: string
|
||||
}
|
||||
|
||||
// 定义用户存储
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
user: null as User | null,
|
||||
token: localStorage.getItem('token') || null,
|
||||
loading: false,
|
||||
error: null as string | null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => !!state.token,
|
||||
isAdmin: (state) => state.user?.role === 'admin'
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 登录
|
||||
async login(credentials: LoginRequest) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
console.log('开始登录请求...', credentials)
|
||||
const response = await axios.post('/api/users/login', credentials)
|
||||
console.log('登录响应:', response.data)
|
||||
const { access_token } = response.data
|
||||
|
||||
// 保存token到本地存储
|
||||
localStorage.setItem('token', access_token)
|
||||
this.token = access_token
|
||||
|
||||
// 获取用户信息
|
||||
await this.fetchUser()
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
this.error = error.response?.data?.detail || '登录失败,请检查用户名和密码'
|
||||
return false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 注册
|
||||
async register(userData: RegisterRequest) {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
console.log('开始注册请求...', userData)
|
||||
const response = await axios.post('/api/users/register', userData)
|
||||
console.log('注册响应:', response.data)
|
||||
console.log('响应状态:', response.status)
|
||||
return true
|
||||
} catch (error: any) {
|
||||
console.error('注册失败:', error)
|
||||
console.error('错误详情:', error.response?.data)
|
||||
console.error('错误状态:', error.response?.status)
|
||||
|
||||
// 提取详细的错误信息
|
||||
const detail = error.response?.data?.detail
|
||||
if (detail) {
|
||||
this.error = detail
|
||||
} else if (error.response?.status === 400) {
|
||||
this.error = '请求参数错误,请检查输入信息'
|
||||
} else {
|
||||
this.error = '注册失败,请稍后重试'
|
||||
}
|
||||
|
||||
return false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
async fetchUser() {
|
||||
this.loading = true
|
||||
this.error = null
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/users/me')
|
||||
this.user = response.data
|
||||
|
||||
// 保存用户信息到本地存储
|
||||
localStorage.setItem('user', JSON.stringify(response.data))
|
||||
|
||||
return true
|
||||
} catch (error: any) {
|
||||
this.error = error.response?.data?.detail || '获取用户信息失败'
|
||||
return false
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
|
||||
// 清除状态
|
||||
this.token = null
|
||||
this.user = null
|
||||
this.error = null
|
||||
},
|
||||
|
||||
// 初始化用户状态
|
||||
init() {
|
||||
const token = localStorage.getItem('token')
|
||||
const userStr = localStorage.getItem('user')
|
||||
|
||||
if (token) {
|
||||
this.token = token
|
||||
}
|
||||
|
||||
if (userStr) {
|
||||
try {
|
||||
this.user = JSON.parse(userStr)
|
||||
} catch {
|
||||
this.logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
79
frontend/src/style.css
Normal file
79
frontend/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
147
frontend/src/views/AdminView.vue
Normal file
147
frontend/src/views/AdminView.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="admin-container">
|
||||
<!-- 页面标题 -->
|
||||
<el-breadcrumb separator="/" class="breadcrumb">
|
||||
<el-breadcrumb-item><router-link to="/">首页</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item>管理员中心</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
|
||||
<div class="admin-content">
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="admin-sidebar">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="sidebar-menu"
|
||||
@select="handleMenuSelect"
|
||||
>
|
||||
<el-menu-item index="/admin/algorithms">
|
||||
<template #icon>
|
||||
<el-icon><data-analysis /></el-icon>
|
||||
</template>
|
||||
<span>算法仓库管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/services">
|
||||
<template #icon>
|
||||
<el-icon><cpu /></el-icon>
|
||||
</template>
|
||||
<span>算法服务管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/api">
|
||||
<template #icon>
|
||||
<el-icon><link /></el-icon>
|
||||
</template>
|
||||
<span>API管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/users">
|
||||
<template #icon>
|
||||
<el-icon><user /></el-icon>
|
||||
</template>
|
||||
<span>用户管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/admin/api-keys">
|
||||
<template #icon>
|
||||
<el-icon><key /></el-icon>
|
||||
</template>
|
||||
<span>API密钥管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="admin-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { DataAnalysis, User, Key, Link, Cpu } from '@element-plus/icons-vue'
|
||||
|
||||
// 获取路由和路由器
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 计算当前激活的菜单
|
||||
const activeMenu = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
|
||||
// 处理菜单选择
|
||||
const handleMenuSelect = (key: string) => {
|
||||
router.push(key)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
width: 200px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.admin-main {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
650
frontend/src/views/AlgorithmCallView.vue
Normal file
650
frontend/src/views/AlgorithmCallView.vue
Normal file
@@ -0,0 +1,650 @@
|
||||
<template>
|
||||
<div class="algorithm-call-container">
|
||||
<!-- 页面标题 -->
|
||||
<el-breadcrumb separator="/" class="breadcrumb">
|
||||
<el-breadcrumb-item><router-link to="/">首页</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item><router-link to="/algorithms">算法列表</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item><router-link :to="`/algorithm/${algorithmStore.currentAlgorithm?.id}`">{{ algorithmStore.currentAlgorithm?.name }}</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item>算法调用</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
|
||||
<el-loading v-if="algorithmStore.loading" :fullscreen="true" text="加载中..." />
|
||||
|
||||
<template v-else-if="algorithmStore.currentAlgorithm">
|
||||
<!-- 算法信息 -->
|
||||
<el-card class="algorithm-info-card">
|
||||
<h1>{{ algorithmStore.currentAlgorithm.name }}</h1>
|
||||
<p class="algorithm-description">{{ algorithmStore.currentAlgorithm.description }}</p>
|
||||
</el-card>
|
||||
|
||||
<!-- 版本选择 -->
|
||||
<el-card class="version-select-card">
|
||||
<template #header>
|
||||
<h2>版本选择</h2>
|
||||
</template>
|
||||
<el-form-item label="选择版本">
|
||||
<el-select v-model="selectedVersionId" placeholder="选择算法版本" @change="handleVersionChange">
|
||||
<el-option
|
||||
v-for="version in algorithmStore.currentAlgorithm.versions"
|
||||
:key="version.id"
|
||||
:label="version.version + (version.is_default ? ' (默认)' : '')"
|
||||
:value="version.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<!-- 输入数据 -->
|
||||
<el-card class="input-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>输入数据</h2>
|
||||
<el-button type="primary" @click="showOpenAIDialog = true">
|
||||
<el-icon><chat-dot-round /></el-icon>
|
||||
使用OpenAI生成
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="input-section">
|
||||
<!-- 文本输入 -->
|
||||
<el-form-item label="文本输入" v-if="inputType === 'text'">
|
||||
<el-input
|
||||
v-model="inputData.text"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入文本数据"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 图片输入 -->
|
||||
<el-form-item label="图片上传" v-if="inputType === 'image'">
|
||||
<el-upload
|
||||
class="image-upload"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleImageUpload"
|
||||
:show-file-list="false"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button size="large">
|
||||
<el-icon><upload /></el-icon>
|
||||
选择图片
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<div v-if="inputData.image" class="image-preview">
|
||||
<img :src="inputData.image" alt="预览" />
|
||||
<el-button type="danger" size="small" @click="inputData.image = ''">删除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 视频输入 -->
|
||||
<el-form-item label="视频上传" v-if="inputType === 'video'">
|
||||
<el-upload
|
||||
class="video-upload"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handleVideoUpload"
|
||||
:show-file-list="false"
|
||||
accept="video/*"
|
||||
>
|
||||
<el-button size="large">
|
||||
<el-icon><upload /></el-icon>
|
||||
选择视频
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<div v-if="inputData.video" class="video-preview">
|
||||
<video :src="inputData.video" controls width="400"></video>
|
||||
<el-button type="danger" size="small" @click="inputData.video = ''">删除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- PLY文件输入 -->
|
||||
<el-form-item label="PLY文件上传" v-if="inputType === 'ply'">
|
||||
<el-upload
|
||||
class="ply-upload"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:on-change="handlePlyUpload"
|
||||
:show-file-list="false"
|
||||
accept=".ply"
|
||||
>
|
||||
<el-button size="large">
|
||||
<el-icon><upload /></el-icon>
|
||||
选择PLY文件
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<div v-if="inputData.ply" class="ply-preview">
|
||||
<el-tag type="success">{{ getPlyFileName(inputData.ply) }}</el-tag>
|
||||
<el-button type="danger" size="small" @click="inputData.ply = ''">删除</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 结构化数据输入 -->
|
||||
<el-form-item label="结构化数据" v-if="inputType === 'structured'">
|
||||
<el-input
|
||||
v-model="structuredInput"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入JSON格式的结构化数据"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 输入类型选择 -->
|
||||
<el-form-item label="输入类型">
|
||||
<el-radio-group v-model="inputType" @change="handleInputTypeChange">
|
||||
<el-radio label="text">文本</el-radio>
|
||||
<el-radio label="image">图片</el-radio>
|
||||
<el-radio label="video">视频</el-radio>
|
||||
<el-radio label="ply">PLY文件</el-radio>
|
||||
<el-radio label="structured">结构化数据</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 参数配置 -->
|
||||
<el-card class="params-card" v-if="currentVersion?.params">
|
||||
<template #header>
|
||||
<h2>参数配置</h2>
|
||||
</template>
|
||||
|
||||
<el-form :model="algorithmParams" label-width="120px">
|
||||
<el-form-item
|
||||
v-for="(paramConfig, paramName) in currentVersion.params"
|
||||
:key="paramName"
|
||||
:label="paramName"
|
||||
>
|
||||
<!-- 数值参数 -->
|
||||
<el-input-number
|
||||
v-if="paramConfig.type === 'number'"
|
||||
v-model="algorithmParams[paramName]"
|
||||
:min="paramConfig.min"
|
||||
:max="paramConfig.max"
|
||||
:step="paramConfig.step || 0.1"
|
||||
/>
|
||||
|
||||
<!-- 字符串参数 -->
|
||||
<el-input
|
||||
v-else-if="paramConfig.type === 'string'"
|
||||
v-model="algorithmParams[paramName]"
|
||||
:placeholder="paramConfig.placeholder"
|
||||
/>
|
||||
|
||||
<!-- 布尔参数 -->
|
||||
<el-switch
|
||||
v-else-if="paramConfig.type === 'boolean'"
|
||||
v-model="algorithmParams[paramName]"
|
||||
/>
|
||||
|
||||
<!-- 下拉选择参数 -->
|
||||
<el-select
|
||||
v-else-if="paramConfig.options"
|
||||
v-model="algorithmParams[paramName]"
|
||||
placeholder="选择选项"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in paramConfig.options"
|
||||
:key="option"
|
||||
:label="option"
|
||||
:value="option"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<!-- 默认输入 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="algorithmParams[paramName]"
|
||||
:placeholder="paramConfig.placeholder"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 调用按钮 -->
|
||||
<div class="call-section">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="calling"
|
||||
@click="handleCallAlgorithm"
|
||||
:disabled="!selectedVersionId || !isInputValid"
|
||||
>
|
||||
调用算法
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 调用结果 -->
|
||||
<el-card v-if="algorithmStore.callResult" class="result-card">
|
||||
<template #header>
|
||||
<h2>调用结果</h2>
|
||||
</template>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="result-meta">
|
||||
<span class="meta-item">
|
||||
<el-icon><timer /></el-icon>
|
||||
响应时间: {{ algorithmStore.callResult.response_time }} 秒
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<el-icon><check /></el-icon>
|
||||
状态: {{ algorithmStore.callResult.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="algorithmStore.callResult.status === 'success'" class="success-result">
|
||||
<pre>{{ JSON.stringify(algorithmStore.callResult.output_data, null, 2) }}</pre>
|
||||
</div>
|
||||
|
||||
<div v-else class="error-result">
|
||||
<el-alert
|
||||
:title="algorithmStore.callResult.status"
|
||||
:description="algorithmStore.callResult.error_message"
|
||||
type="error"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<div v-else class="error-state">
|
||||
<el-icon class="error-icon"><warning /></el-icon>
|
||||
<p>获取算法信息失败</p>
|
||||
<router-link to="/algorithms" class="back-btn">
|
||||
<el-button type="primary">返回算法列表</el-button>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI对话框 -->
|
||||
<el-dialog
|
||||
v-model="showOpenAIDialog"
|
||||
title="使用OpenAI生成输入数据"
|
||||
width="50%"
|
||||
>
|
||||
<el-form :model="openAIForm">
|
||||
<el-form-item label="描述">
|
||||
<el-input
|
||||
v-model="openAIForm.prompt"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请描述您需要的输入数据"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据类型">
|
||||
<el-select v-model="openAIForm.dataType" placeholder="选择数据类型">
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="结构化数据" value="structured" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showOpenAIDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="generateDataWithOpenAI" :loading="generatingData">生成</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAlgorithmStore } from '../stores/algorithm'
|
||||
import { ChatDotRound, Upload, Warning, Timer, Check } from '@element-plus/icons-vue'
|
||||
|
||||
// 获取路由和存储
|
||||
const route = useRoute()
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 状态管理
|
||||
const selectedVersionId = ref('')
|
||||
const inputType = ref('text')
|
||||
const inputData = ref({ text: '', image: '', video: '', ply: '', structured: {} })
|
||||
const algorithmParams = ref({})
|
||||
const calling = ref(false)
|
||||
const showOpenAIDialog = ref(false)
|
||||
const generatingData = ref(false)
|
||||
const structuredInput = ref('')
|
||||
|
||||
// OpenAI表单
|
||||
const openAIForm = ref({
|
||||
prompt: '',
|
||||
dataType: 'text'
|
||||
})
|
||||
|
||||
// 计算当前选中的版本
|
||||
const currentVersion = computed(() => {
|
||||
if (!algorithmStore.currentAlgorithm || !selectedVersionId.value) return null
|
||||
return algorithmStore.currentAlgorithm.versions.find(v => v.id === selectedVersionId.value)
|
||||
})
|
||||
|
||||
// 检查输入是否有效
|
||||
const isInputValid = computed(() => {
|
||||
if (inputType.value === 'text') {
|
||||
return inputData.value.text.trim() !== ''
|
||||
} else if (inputType.value === 'image') {
|
||||
return inputData.value.image !== ''
|
||||
} else if (inputType.value === 'video') {
|
||||
return inputData.value.video !== ''
|
||||
} else if (inputType.value === 'ply') {
|
||||
return inputData.value.ply !== ''
|
||||
} else if (inputType.value === 'structured') {
|
||||
try {
|
||||
JSON.parse(structuredInput.value)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// 处理版本变更
|
||||
const handleVersionChange = () => {
|
||||
// 重置参数
|
||||
if (currentVersion.value?.params) {
|
||||
const params = {}
|
||||
for (const [key, config] of Object.entries(currentVersion.value.params)) {
|
||||
params[key] = config.default || ''
|
||||
}
|
||||
algorithmParams.value = params
|
||||
}
|
||||
}
|
||||
|
||||
// 处理输入类型变更
|
||||
const handleInputTypeChange = () => {
|
||||
// 重置输入数据
|
||||
inputData.value = { text: '', image: '', video: '', ply: '', structured: {} }
|
||||
structuredInput.value = ''
|
||||
}
|
||||
|
||||
// 处理图片上传
|
||||
const handleImageUpload = (file: any) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
inputData.value.image = e.target?.result as string
|
||||
}
|
||||
reader.readAsDataURL(file.raw)
|
||||
}
|
||||
|
||||
// 处理视频上传
|
||||
const handleVideoUpload = (file: any) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
inputData.value.video = e.target?.result as string
|
||||
}
|
||||
reader.readAsDataURL(file.raw)
|
||||
}
|
||||
|
||||
// 处理PLY文件上传
|
||||
const handlePlyUpload = (file: any) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
inputData.value.ply = e.target?.result as string
|
||||
}
|
||||
reader.readAsDataURL(file.raw)
|
||||
}
|
||||
|
||||
// 从data URL中提取PLY文件名
|
||||
const getPlyFileName = (dataUrl: string) => {
|
||||
// 这里简单返回文件名,实际应用中可以从data URL或文件对象中提取更多信息
|
||||
return 'PLY文件'
|
||||
}
|
||||
|
||||
// 处理算法调用
|
||||
const handleCallAlgorithm = async () => {
|
||||
if (!selectedVersionId.value || !algorithmStore.currentAlgorithm) return
|
||||
|
||||
calling.value = true
|
||||
|
||||
// 准备输入数据
|
||||
let finalInputData: any = {}
|
||||
if (inputType.value === 'text') {
|
||||
finalInputData.text = inputData.value.text
|
||||
} else if (inputType.value === 'image') {
|
||||
finalInputData.image = inputData.value.image
|
||||
} else if (inputType.value === 'video') {
|
||||
finalInputData.video = inputData.value.video
|
||||
} else if (inputType.value === 'ply') {
|
||||
finalInputData.ply = inputData.value.ply
|
||||
} else if (inputType.value === 'structured') {
|
||||
try {
|
||||
finalInputData.structured = JSON.parse(structuredInput.value)
|
||||
} catch {
|
||||
finalInputData.structured = {}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用算法
|
||||
await algorithmStore.callAlgorithm({
|
||||
algorithm_id: algorithmStore.currentAlgorithm.id,
|
||||
version_id: selectedVersionId.value,
|
||||
input_data: finalInputData,
|
||||
params: algorithmParams.value
|
||||
})
|
||||
|
||||
calling.value = false
|
||||
}
|
||||
|
||||
// 使用OpenAI生成数据
|
||||
const generateDataWithOpenAI = async () => {
|
||||
if (!openAIForm.value.prompt) return
|
||||
|
||||
generatingData.value = true
|
||||
|
||||
// 调用OpenAI生成数据
|
||||
const result = await algorithmStore.generateSimulationData(openAIForm.value.prompt, openAIForm.value.dataType)
|
||||
|
||||
if (result) {
|
||||
if (openAIForm.value.dataType === 'text') {
|
||||
inputType.value = 'text'
|
||||
inputData.value.text = result.data
|
||||
} else if (openAIForm.value.dataType === 'structured') {
|
||||
inputType.value = 'structured'
|
||||
structuredInput.value = JSON.stringify(result.data, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
generatingData.value = false
|
||||
showOpenAIDialog.value = false
|
||||
}
|
||||
|
||||
// 加载算法详情
|
||||
onMounted(async () => {
|
||||
const algorithmId = route.params.id as string
|
||||
const versionId = route.query.version_id as string
|
||||
|
||||
if (algorithmId) {
|
||||
await algorithmStore.fetchAlgorithmDetail(algorithmId)
|
||||
|
||||
// 如果指定了版本,选择该版本
|
||||
if (versionId) {
|
||||
selectedVersionId.value = versionId
|
||||
} else if (algorithmStore.currentAlgorithm?.versions.length > 0) {
|
||||
// 否则选择默认版本或第一个版本
|
||||
const defaultVersion = algorithmStore.currentAlgorithm.versions.find(v => v.is_default)
|
||||
selectedVersionId.value = defaultVersion?.id || algorithmStore.currentAlgorithm.versions[0].id
|
||||
}
|
||||
|
||||
// 初始化参数
|
||||
handleVersionChange()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.algorithm-call-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.algorithm-info-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.algorithm-info-card h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.algorithm-description {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.version-select-card,
|
||||
.input-card,
|
||||
.params-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 300px;
|
||||
max-height: 200px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.video-preview video {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.video-preview .el-button {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.ply-upload {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ply-preview {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ply-preview .el-tag {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.call-section {
|
||||
margin: 30px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.success-result pre {
|
||||
background-color: #f0f9eb;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e1f3d8;
|
||||
color: #67c23a;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.error-result {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.error-state p {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
252
frontend/src/views/AlgorithmDetailView.vue
Normal file
252
frontend/src/views/AlgorithmDetailView.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<div class="algorithm-detail-container">
|
||||
<!-- 页面标题 -->
|
||||
<el-breadcrumb separator="/" class="breadcrumb">
|
||||
<el-breadcrumb-item><router-link to="/">首页</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item><router-link to="/algorithms">算法列表</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ algorithmStore.currentAlgorithm?.name }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
|
||||
<el-loading v-if="algorithmStore.loading" :fullscreen="true" text="加载中..." />
|
||||
|
||||
<template v-else-if="algorithmStore.currentAlgorithm">
|
||||
<!-- 算法基本信息 -->
|
||||
<el-card class="algorithm-info-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h1>{{ algorithmStore.currentAlgorithm.name }}</h1>
|
||||
<span class="algorithm-type">{{ getAlgorithmTypeName(algorithmStore.currentAlgorithm.type) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="algorithm-info">
|
||||
<p class="algorithm-description">{{ algorithmStore.currentAlgorithm.description }}</p>
|
||||
|
||||
<div class="algorithm-meta">
|
||||
<span class="meta-item">
|
||||
<el-icon><time /></el-icon>
|
||||
创建时间: {{ formatDate(algorithmStore.currentAlgorithm.created_at) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<el-icon><timer /></el-icon>
|
||||
更新时间: {{ formatDate(algorithmStore.currentAlgorithm.updated_at) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<el-icon><check /></el-icon>
|
||||
状态: {{ algorithmStore.currentAlgorithm.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="algorithm-actions">
|
||||
<router-link :to="`/algorithm/${algorithmStore.currentAlgorithm.id}/call`" class="call-btn">
|
||||
<el-button type="primary" size="large">立即调用</el-button>
|
||||
</router-link>
|
||||
<router-link v-if="userStore.isAdmin" :to="`/algorithm/${algorithmStore.currentAlgorithm.id}/versions`" class="versions-btn">
|
||||
<el-button size="large">版本管理</el-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 算法版本列表 -->
|
||||
<div class="versions-section">
|
||||
<h2>算法版本</h2>
|
||||
|
||||
<el-table :data="algorithmStore.currentAlgorithm.versions" style="width: 100%">
|
||||
<el-table-column prop="version" label="版本号" width="120" />
|
||||
<el-table-column prop="url" label="API地址" />
|
||||
<el-table-column prop="is_default" label="默认版本" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.is_default" type="success">是</el-tag>
|
||||
<el-tag v-else type="info">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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="150">
|
||||
<template #default="scope">
|
||||
<router-link :to="`/algorithm/${algorithmStore.currentAlgorithm.id}/call?version_id=${scope.row.id}`">
|
||||
<el-button size="small" type="primary">调用</el-button>
|
||||
</router-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="error-state">
|
||||
<el-icon class="error-icon"><warning /></el-icon>
|
||||
<p>获取算法详情失败</p>
|
||||
<router-link to="/algorithms" class="back-btn">
|
||||
<el-button type="primary">返回算法列表</el-button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAlgorithmStore } from '../stores/algorithm'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { Time, Timer, Check, Warning } from '@element-plus/icons-vue'
|
||||
|
||||
// 获取路由和存储
|
||||
const route = useRoute()
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 算法类型映射
|
||||
const algorithmTypes = {
|
||||
computer_vision: '计算机视觉',
|
||||
nlp: '自然语言处理',
|
||||
ml: '机器学习',
|
||||
reinforcement_learning: '强化学习',
|
||||
edge_computing: '边缘计算',
|
||||
medical: '医疗算法',
|
||||
autonomous_driving: '自动驾驶算法'
|
||||
}
|
||||
|
||||
// 获取算法类型的中文名称
|
||||
const getAlgorithmTypeName = (type: string) => {
|
||||
return algorithmTypes[type as keyof typeof algorithmTypes] || type
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 加载算法详情
|
||||
onMounted(async () => {
|
||||
const algorithmId = route.params.id as string
|
||||
if (algorithmId) {
|
||||
await algorithmStore.fetchAlgorithmDetail(algorithmId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.algorithm-detail-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.algorithm-info-card {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header h1 {
|
||||
font-size: 28px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.algorithm-type {
|
||||
padding: 6px 16px;
|
||||
background-color: #ecf5ff;
|
||||
color: #409EFF;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.algorithm-info {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.algorithm-description {
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 30px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.algorithm-meta {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.algorithm-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.versions-section {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.versions-section h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.error-state p {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.algorithm-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.algorithm-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.algorithm-actions a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.algorithm-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
730
frontend/src/views/AlgorithmVersionsView.vue
Normal file
730
frontend/src/views/AlgorithmVersionsView.vue
Normal file
@@ -0,0 +1,730 @@
|
||||
<template>
|
||||
<div class="algorithm-versions-container">
|
||||
<!-- 页面标题 -->
|
||||
<el-breadcrumb separator="/" class="breadcrumb">
|
||||
<el-breadcrumb-item><router-link to="/">首页</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item><router-link to="/algorithms">算法列表</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item><router-link :to="`/algorithm/${algorithmStore.currentAlgorithm?.id}`">{{ algorithmStore.currentAlgorithm?.name }}</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item>版本管理</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
|
||||
<el-loading v-if="algorithmStore.loading" :fullscreen="true" text="加载中..." />
|
||||
|
||||
<template v-else-if="algorithmStore.currentAlgorithm">
|
||||
<!-- 算法信息 -->
|
||||
<el-card class="algorithm-info-card">
|
||||
<h1>{{ algorithmStore.currentAlgorithm.name }}</h1>
|
||||
<p class="algorithm-description">{{ algorithmStore.currentAlgorithm.description }}</p>
|
||||
</el-card>
|
||||
|
||||
<!-- 版本管理 -->
|
||||
<el-card class="versions-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>版本列表</h2>
|
||||
<el-button type="primary" @click="showAddVersionDialog = true">
|
||||
<el-icon><plus /></el-icon>
|
||||
添加版本
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="algorithmStore.currentAlgorithm.versions" style="width: 100%">
|
||||
<el-table-column prop="version" label="版本号" width="120" />
|
||||
<el-table-column prop="url" label="API地址" />
|
||||
<el-table-column prop="is_default" label="默认版本" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.is_default" type="success">是</el-tag>
|
||||
<el-tag v-else type="info">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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="380">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editVersion(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="primary" @click="executeCode(scope.row)">执行代码</el-button>
|
||||
<el-button size="small" type="success" @click="runAlgorithm(scope.row)">执行算法</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteVersion(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<div v-else class="error-state">
|
||||
<el-icon class="error-icon"><warning /></el-icon>
|
||||
<p>获取算法详情失败</p>
|
||||
<router-link to="/algorithms" class="back-btn">
|
||||
<el-button type="primary">返回算法列表</el-button>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 添加版本对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddVersionDialog"
|
||||
title="添加版本"
|
||||
width="70%"
|
||||
>
|
||||
<el-form :model="versionForm" :rules="versionRules" ref="versionFormRef">
|
||||
<el-form-item label="版本号" prop="version">
|
||||
<el-input v-model="versionForm.version" placeholder="请输入版本号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API地址" prop="url">
|
||||
<el-input v-model="versionForm.url" placeholder="请输入算法API地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="默认版本">
|
||||
<el-switch v-model="versionForm.is_default" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数配置">
|
||||
<el-input
|
||||
v-model="versionForm.params"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入JSON格式的参数配置"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="输入模式">
|
||||
<el-input
|
||||
v-model="versionForm.input_schema"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入JSON格式的输入模式"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="输出模式">
|
||||
<el-input
|
||||
v-model="versionForm.output_schema"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入JSON格式的输出模式"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="算法代码">
|
||||
<div ref="codeEditorContainer" style="height: 400px; width: 100%"></div>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名字">
|
||||
<el-input v-model="versionForm.model_name" placeholder="请输入API训练后的模型名字" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型文件">
|
||||
<el-upload
|
||||
class="model-file-uploader"
|
||||
action="/api/algorithms/upload-model"
|
||||
:headers="{ 'Authorization': `Bearer ${localStorage.getItem('token')}` }"
|
||||
:on-success="handleModelFileUpload"
|
||||
:on-error="handleModelFileUploadError"
|
||||
:file-list="modelFileList"
|
||||
:auto-upload="false"
|
||||
ref="modelFileUploadRef"
|
||||
accept=".pt,.pth,.h5,.hdf5,.onnx,.pb,.tflite,.joblib,.pkl,.zip,.tar.gz"
|
||||
>
|
||||
<el-button type="primary">
|
||||
<el-icon><upload /></el-icon>
|
||||
选择文件
|
||||
</el-button>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持的文件类型:.pt, .pth, .h5, .hdf5, .onnx, .pb, .tflite, .joblib, .pkl, .zip, .tar.gz
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="API用法">
|
||||
<el-input
|
||||
v-model="versionForm.api_doc"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入模型的API用法文档,包括input参数和返回内容类型"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showAddVersionDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="addVersion">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑版本对话框 -->
|
||||
<el-dialog
|
||||
v-model="showEditVersionDialog"
|
||||
title="编辑版本"
|
||||
width="70%"
|
||||
>
|
||||
<el-form :model="editVersionForm" :rules="versionRules" ref="editVersionFormRef">
|
||||
<el-form-item label="版本号" prop="version">
|
||||
<el-input v-model="editVersionForm.version" placeholder="请输入版本号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API地址" prop="url">
|
||||
<el-input v-model="editVersionForm.url" placeholder="请输入算法API地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="默认版本">
|
||||
<el-switch v-model="editVersionForm.is_default" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数配置">
|
||||
<el-input
|
||||
v-model="editVersionForm.params"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入JSON格式的参数配置"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="输入模式">
|
||||
<el-input
|
||||
v-model="editVersionForm.input_schema"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入JSON格式的输入模式"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="输出模式">
|
||||
<el-input
|
||||
v-model="editVersionForm.output_schema"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入JSON格式的输出模式"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="算法代码">
|
||||
<div ref="editCodeEditorContainer" style="height: 400px; width: 100%"></div>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名字">
|
||||
<el-input v-model="editVersionForm.model_name" placeholder="请输入API训练后的模型名字" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型文件">
|
||||
<el-input v-model="editVersionForm.model_file" placeholder="请输入模型文件路径" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API用法">
|
||||
<el-input
|
||||
v-model="editVersionForm.api_doc"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入模型的API用法文档,包括input参数和返回内容类型"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showEditVersionDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="updateVersion">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 代码执行结果对话框 -->
|
||||
<el-dialog
|
||||
v-model="showCodeExecutionDialog"
|
||||
title="代码执行结果"
|
||||
width="70%"
|
||||
>
|
||||
<div class="code-execution-result">
|
||||
<div v-if="codeExecutionLoading" class="loading-state">
|
||||
<el-icon class="loading-icon"><loading /></el-icon>
|
||||
<p>执行中...</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form-item label="执行状态">
|
||||
<el-tag :type="codeExecutionResult.success ? 'success' : 'danger'">
|
||||
{{ codeExecutionResult.success ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="执行输出">
|
||||
<el-input
|
||||
v-model="codeExecutionResult.output"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
readonly
|
||||
placeholder="执行输出"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="codeExecutionResult.error" label="错误信息">
|
||||
<el-input
|
||||
v-model="codeExecutionResult.error"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
readonly
|
||||
placeholder="错误信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showCodeExecutionDialog = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAlgorithmStore } from '../stores/algorithm'
|
||||
import { Plus, Warning, Loading, Upload } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as monaco from 'monaco-editor'
|
||||
|
||||
// 获取路由和存储
|
||||
const route = useRoute()
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 状态管理
|
||||
const showAddVersionDialog = ref(false)
|
||||
const showEditVersionDialog = ref(false)
|
||||
const showCodeExecutionDialog = ref(false)
|
||||
const versionFormRef = ref()
|
||||
const editVersionFormRef = ref()
|
||||
const codeEditorContainer = ref<HTMLElement>()
|
||||
const editCodeEditorContainer = ref<HTMLElement>()
|
||||
const codeEditor = ref<monaco.editor.IStandaloneCodeEditor>()
|
||||
const editCodeEditor = ref<monaco.editor.IStandaloneCodeEditor>()
|
||||
const currentVersionId = ref('')
|
||||
const modelFileUploadRef = ref<any>(null)
|
||||
const modelFileList = ref<any[]>([])
|
||||
const versionForm = ref({
|
||||
version: '',
|
||||
url: '',
|
||||
is_default: false,
|
||||
params: '{}',
|
||||
input_schema: '{}',
|
||||
output_schema: '{}',
|
||||
code: '',
|
||||
model_name: '',
|
||||
model_file: '',
|
||||
api_doc: ''
|
||||
})
|
||||
const editVersionForm = ref({
|
||||
version: '',
|
||||
url: '',
|
||||
is_default: false,
|
||||
params: '{}',
|
||||
input_schema: '{}',
|
||||
output_schema: '{}',
|
||||
code: '',
|
||||
model_name: '',
|
||||
model_file: '',
|
||||
api_doc: ''
|
||||
})
|
||||
const codeExecutionLoading = ref(false)
|
||||
const codeExecutionResult = ref({
|
||||
success: false,
|
||||
output: '',
|
||||
error: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const versionRules = ref({
|
||||
version: [
|
||||
{ required: true, message: '请输入版本号', trigger: 'blur' }
|
||||
],
|
||||
url: [
|
||||
{ required: true, message: '请输入API地址', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 添加版本
|
||||
const addVersion = async () => {
|
||||
if (!versionFormRef.value) return
|
||||
|
||||
// 从编辑器中获取代码内容
|
||||
if (codeEditor.value) {
|
||||
versionForm.value.code = codeEditor.value.getValue()
|
||||
}
|
||||
|
||||
// 验证表单
|
||||
await versionFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
// 解析JSON字段
|
||||
try {
|
||||
const params = JSON.parse(versionForm.value.params)
|
||||
const input_schema = JSON.parse(versionForm.value.input_schema)
|
||||
const output_schema = JSON.parse(versionForm.value.output_schema)
|
||||
|
||||
// 这里应该调用后端API添加版本
|
||||
// 暂时模拟添加
|
||||
console.log('添加版本:', {
|
||||
...versionForm.value,
|
||||
params,
|
||||
input_schema,
|
||||
output_schema
|
||||
})
|
||||
|
||||
// 关闭对话框
|
||||
showAddVersionDialog.value = false
|
||||
|
||||
// 重置表单
|
||||
versionForm.value = {
|
||||
version: '',
|
||||
url: '',
|
||||
is_default: false,
|
||||
params: '{}',
|
||||
input_schema: '{}',
|
||||
output_schema: '{}',
|
||||
code: '',
|
||||
model_name: '',
|
||||
model_file: '',
|
||||
api_doc: ''
|
||||
}
|
||||
|
||||
// 重置编辑器内容
|
||||
if (codeEditor.value) {
|
||||
codeEditor.value.setValue('')
|
||||
}
|
||||
|
||||
// 重新加载版本列表
|
||||
if (algorithmStore.currentAlgorithm) {
|
||||
await algorithmStore.fetchAlgorithmDetail(algorithmStore.currentAlgorithm.id)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('JSON格式错误,请检查输入')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化代码编辑器
|
||||
const initCodeEditor = () => {
|
||||
if (codeEditorContainer.value && !codeEditor.value) {
|
||||
codeEditor.value = monaco.editor.create(codeEditorContainer.value, {
|
||||
value: versionForm.value.code,
|
||||
language: 'python',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
minimap: {
|
||||
enabled: true
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
renderIndentGuides: true,
|
||||
fontSize: 14,
|
||||
tabSize: 4
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化编辑代码编辑器
|
||||
const initEditCodeEditor = () => {
|
||||
if (editCodeEditorContainer.value && !editCodeEditor.value) {
|
||||
editCodeEditor.value = monaco.editor.create(editCodeEditorContainer.value, {
|
||||
value: editVersionForm.value.code,
|
||||
language: 'python',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
minimap: {
|
||||
enabled: true
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
renderIndentGuides: true,
|
||||
fontSize: 14,
|
||||
tabSize: 4
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁代码编辑器
|
||||
const destroyCodeEditor = () => {
|
||||
if (codeEditor.value) {
|
||||
codeEditor.value.dispose()
|
||||
codeEditor.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁编辑代码编辑器
|
||||
const destroyEditCodeEditor = () => {
|
||||
if (editCodeEditor.value) {
|
||||
editCodeEditor.value.dispose()
|
||||
editCodeEditor.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 监听添加对话框显示状态,初始化或更新编辑器
|
||||
watch(showAddVersionDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
// 对话框显示后,等待DOM更新,然后初始化编辑器
|
||||
setTimeout(() => {
|
||||
initCodeEditor()
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
// 监听编辑对话框显示状态,初始化或更新编辑器
|
||||
watch(showEditVersionDialog, (newVal) => {
|
||||
if (newVal) {
|
||||
// 对话框显示后,等待DOM更新,然后初始化编辑器
|
||||
setTimeout(() => {
|
||||
initEditCodeEditor()
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑版本
|
||||
const editVersion = (version: any) => {
|
||||
// 填充编辑表单
|
||||
editVersionForm.value = {
|
||||
version: version.version,
|
||||
url: version.url,
|
||||
is_default: version.is_default,
|
||||
params: JSON.stringify(version.params || {}, null, 2),
|
||||
input_schema: JSON.stringify(version.input_schema || {}, null, 2),
|
||||
output_schema: JSON.stringify(version.output_schema || {}, null, 2),
|
||||
code: version.code || '',
|
||||
model_name: version.model_name || '',
|
||||
model_file: version.model_file || '',
|
||||
api_doc: version.api_doc || ''
|
||||
}
|
||||
|
||||
// 保存当前版本ID
|
||||
currentVersionId.value = version.id
|
||||
|
||||
// 显示编辑对话框
|
||||
showEditVersionDialog.value = true
|
||||
}
|
||||
|
||||
// 更新版本
|
||||
const updateVersion = async () => {
|
||||
if (!editVersionFormRef.value) return
|
||||
|
||||
// 从编辑器中获取代码内容
|
||||
if (editCodeEditor.value) {
|
||||
editVersionForm.value.code = editCodeEditor.value.getValue()
|
||||
}
|
||||
|
||||
// 验证表单
|
||||
await editVersionFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
// 解析JSON字段
|
||||
try {
|
||||
const params = JSON.parse(editVersionForm.value.params)
|
||||
const input_schema = JSON.parse(editVersionForm.value.input_schema)
|
||||
const output_schema = JSON.parse(editVersionForm.value.output_schema)
|
||||
|
||||
// 这里应该调用后端API更新版本
|
||||
// 暂时模拟更新
|
||||
console.log('更新版本:', {
|
||||
id: currentVersionId.value,
|
||||
...editVersionForm.value,
|
||||
params,
|
||||
input_schema,
|
||||
output_schema
|
||||
})
|
||||
|
||||
// 关闭对话框
|
||||
showEditVersionDialog.value = false
|
||||
|
||||
// 重新加载版本列表
|
||||
if (algorithmStore.currentAlgorithm) {
|
||||
await algorithmStore.fetchAlgorithmDetail(algorithmStore.currentAlgorithm.id)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('JSON格式错误,请检查输入')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 执行代码
|
||||
const executeCode = async (version: any) => {
|
||||
// 显示执行结果对话框
|
||||
showCodeExecutionDialog.value = true
|
||||
codeExecutionLoading.value = true
|
||||
codeExecutionResult.value = {
|
||||
success: false,
|
||||
output: '',
|
||||
error: ''
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查代码是否为空
|
||||
if (!version.code) {
|
||||
codeExecutionResult.value = {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '代码为空,无法执行'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 调用后端API执行代码
|
||||
const response = await fetch('/api/algorithms/execute-code', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
},
|
||||
body: JSON.stringify({ code: version.code })
|
||||
})
|
||||
|
||||
// 处理响应
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
codeExecutionResult.value = result
|
||||
} else {
|
||||
const error = await response.json()
|
||||
codeExecutionResult.value = {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error.detail || '执行请求失败'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
codeExecutionResult.value = {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `执行错误: ${error}`
|
||||
}
|
||||
} finally {
|
||||
codeExecutionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除版本
|
||||
const deleteVersion = (versionId: string) => {
|
||||
// 这里应该调用后端API删除版本,暂时打印
|
||||
console.log('删除版本:', versionId)
|
||||
}
|
||||
|
||||
// 处理模型文件上传成功
|
||||
const handleModelFileUpload = (response: any, uploadFile: any) => {
|
||||
if (response.success) {
|
||||
versionForm.value.model_file = response.file_path
|
||||
modelFileList.value = [{ name: uploadFile.name, url: response.file_path }]
|
||||
ElMessage.success('模型文件上传成功')
|
||||
} else {
|
||||
ElMessage.error('模型文件上传失败: ' + response.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理模型文件上传失败
|
||||
const handleModelFileUploadError = (error: any) => {
|
||||
ElMessage.error('模型文件上传失败: ' + error.message)
|
||||
}
|
||||
|
||||
// 手动触发模型文件上传
|
||||
const uploadModelFile = () => {
|
||||
if (modelFileUploadRef.value) {
|
||||
modelFileUploadRef.value.submit()
|
||||
}
|
||||
}
|
||||
|
||||
// 执行算法
|
||||
const runAlgorithm = async (version: any) => {
|
||||
// 这里应该调用后端API执行算法
|
||||
// 暂时模拟执行
|
||||
ElMessage.info('执行算法: ' + version.version)
|
||||
console.log('执行算法:', version)
|
||||
}
|
||||
|
||||
// 加载算法详情
|
||||
onMounted(async () => {
|
||||
const algorithmId = route.params.id as string
|
||||
if (algorithmId) {
|
||||
await algorithmStore.fetchAlgorithmDetail(algorithmId)
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时销毁编辑器
|
||||
onUnmounted(() => {
|
||||
destroyCodeEditor()
|
||||
destroyEditCodeEditor()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.algorithm-versions-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.algorithm-info-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.algorithm-info-card h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.algorithm-description {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.versions-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.error-state p {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 代码执行结果样式 */
|
||||
.code-execution-result {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.loading-state p {
|
||||
font-size: 16px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
300
frontend/src/views/AlgorithmsView.vue
Normal file
300
frontend/src/views/AlgorithmsView.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<template>
|
||||
<div class="algorithms-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>算法列表</h1>
|
||||
|
||||
<!-- 筛选和搜索 -->
|
||||
<div class="filter-section">
|
||||
<el-form :inline="true" class="filter-form">
|
||||
<el-form-item label="算法类型">
|
||||
<el-select v-model="selectedType" 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="nlp" />
|
||||
<el-option label="机器学习" value="ml" />
|
||||
<el-option label="强化学习" value="reinforcement_learning" />
|
||||
<el-option label="自动驾驶算法" value="autonomous_driving" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="搜索">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="输入算法名称或描述"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="handleSearch"><el-icon><search /></el-icon></el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 算法列表 -->
|
||||
<div class="algorithms-list">
|
||||
<el-loading v-if="algorithmStore.loading" :fullscreen="true" text="加载中..." />
|
||||
|
||||
<div v-else-if="algorithmStore.algorithms.length === 0" class="empty-state">
|
||||
<el-icon class="empty-icon"><document /></el-icon>
|
||||
<p>暂无算法数据</p>
|
||||
</div>
|
||||
|
||||
<el-card v-else v-for="algorithm in filteredAlgorithms" :key="algorithm.id" class="algorithm-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>{{ algorithm.name }}</h2>
|
||||
<span class="algorithm-type">{{ getAlgorithmTypeName(algorithm.type) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="card-body">
|
||||
<p class="algorithm-description">{{ algorithm.description }}</p>
|
||||
|
||||
<div class="algorithm-meta">
|
||||
<span class="meta-item">
|
||||
<el-icon><time /></el-icon>
|
||||
创建时间: {{ formatDate(algorithm.created_at) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<el-icon><folder /></el-icon>
|
||||
版本数: {{ algorithm.versions.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card-actions">
|
||||
<router-link :to="`/algorithm/${algorithm.id}`" class="detail-btn">
|
||||
<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>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-section">
|
||||
<el-pagination
|
||||
v-if="algorithmStore.algorithms.length > 0"
|
||||
layout="prev, pager, next"
|
||||
:total="algorithmStore.algorithms.length"
|
||||
:page-size="pageSize"
|
||||
:current-page="currentPage"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</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'
|
||||
|
||||
// 获取算法存储
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 筛选和分页
|
||||
const selectedType = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
// 算法类型映射
|
||||
const algorithmTypes = {
|
||||
computer_vision: '计算机视觉',
|
||||
nlp: '自然语言处理',
|
||||
ml: '机器学习',
|
||||
reinforcement_learning: '强化学习',
|
||||
edge_computing: '边缘计算',
|
||||
medical: '医疗算法',
|
||||
autonomous_driving: '自动驾驶算法'
|
||||
}
|
||||
|
||||
// 获取算法类型的中文名称
|
||||
const getAlgorithmTypeName = (type: string) => {
|
||||
return algorithmTypes[type as keyof typeof algorithmTypes] || type
|
||||
}
|
||||
|
||||
// 筛选算法
|
||||
const filteredAlgorithms = computed(() => {
|
||||
let algorithms = algorithmStore.algorithms
|
||||
|
||||
// 按类型筛选
|
||||
if (selectedType.value) {
|
||||
algorithms = algorithms.filter(algorithm => algorithm.type === selectedType.value)
|
||||
}
|
||||
|
||||
// 按关键词搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
algorithms = algorithms.filter(algorithm =>
|
||||
algorithm.name.toLowerCase().includes(keyword) ||
|
||||
algorithm.description.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
// 分页
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return algorithms.slice(start, end)
|
||||
})
|
||||
|
||||
// 处理类型变更
|
||||
const handleTypeChange = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
// 处理分页变更
|
||||
const handlePageChange = (page: number) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 加载算法列表
|
||||
onMounted(async () => {
|
||||
await algorithmStore.fetchAlgorithms()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.algorithms-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.algorithms-container h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 30px;
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.algorithms-list {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.algorithm-card {
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.algorithm-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.algorithm-type {
|
||||
padding: 4px 12px;
|
||||
background-color: #ecf5ff;
|
||||
color: #409EFF;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.algorithm-description {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.algorithm-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filter-form {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.algorithm-meta {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-actions a {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
403
frontend/src/views/HomeView.vue
Normal file
403
frontend/src/views/HomeView.vue
Normal file
@@ -0,0 +1,403 @@
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<!-- 英雄区域 -->
|
||||
<section class="hero-section">
|
||||
<div class="hero-content">
|
||||
<h1>智能算法展示平台</h1>
|
||||
<p>通过「仿真输入 - 一键调用 - 效果可视化」快速感知算法价值</p>
|
||||
<div class="hero-buttons">
|
||||
<router-link to="/algorithms" class="primary-btn">浏览算法</router-link>
|
||||
<router-link to="/register" class="secondary-btn">注册体验</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 核心功能 -->
|
||||
<section class="features-section">
|
||||
<h2>核心功能</h2>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<el-icon class="icon"><data-analysis /></el-icon>
|
||||
</div>
|
||||
<h3>算法API封装调用</h3>
|
||||
<p>将所有算法封装成统一的API接口,支持一键调用,方便快捷</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<el-icon class="icon"><chat-dot-round /></el-icon>
|
||||
</div>
|
||||
<h3>OpenAI仿真输入获取</h3>
|
||||
<p>集成OpenAI API,通过文本描述生成仿真输入数据,提高测试效率</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<el-icon class="icon"><pie-chart /></el-icon>
|
||||
</div>
|
||||
<h3>多维度效果展示</h3>
|
||||
<p>支持图表、图像对比、数值分析等多种展示方式,直观呈现算法效果</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 热门算法 -->
|
||||
<section class="algorithms-section">
|
||||
<div class="section-header">
|
||||
<h2>热门算法</h2>
|
||||
<router-link to="/algorithms" class="view-all">查看全部</router-link>
|
||||
</div>
|
||||
|
||||
<div class="algorithms-grid">
|
||||
<div v-for="algorithm in popularAlgorithms" :key="algorithm.id" class="algorithm-card">
|
||||
<h3>{{ algorithm.name }}</h3>
|
||||
<p class="algorithm-type">{{ algorithm.type }}</p>
|
||||
<p class="algorithm-desc">{{ algorithm.description }}</p>
|
||||
<div class="algorithm-actions">
|
||||
<router-link :to="`/algorithm/${algorithm.id}`" class="detail-btn">查看详情</router-link>
|
||||
<router-link :to="`/algorithm/${algorithm.id}/call`" class="call-btn">立即调用</router-link>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useAlgorithmStore } from '../stores/algorithm'
|
||||
import { DataAnalysis, ChatDotRound, PieChart } from '@element-plus/icons-vue'
|
||||
|
||||
// 模拟热门算法数据
|
||||
const popularAlgorithms = ref([
|
||||
{
|
||||
id: 'algorithm-001',
|
||||
name: '图像分类算法',
|
||||
type: 'computer_vision',
|
||||
description: '基于深度学习的图像分类算法,支持多种物体类别的识别'
|
||||
},
|
||||
{
|
||||
id: 'algorithm-002',
|
||||
name: '文本情感分析算法',
|
||||
type: 'nlp',
|
||||
description: '分析文本的情感倾向,支持积极、消极、中性三种情感分类'
|
||||
},
|
||||
{
|
||||
id: 'algorithm-003',
|
||||
name: '推荐算法',
|
||||
type: 'ml',
|
||||
description: '基于用户行为的推荐算法,提供个性化推荐结果'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 英雄区域 */
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, #409EFF 0%, #667EEA 100%);
|
||||
color: #fff;
|
||||
padding: 100px 20px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hero-content p {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.primary-btn,
|
||||
.secondary-btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
background-color: #fff;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.primary-btn:hover {
|
||||
background-color: #f5f7fa;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.secondary-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 功能区域 */
|
||||
.features-section {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.features-section h2 {
|
||||
text-align: center;
|
||||
font-size: 32px;
|
||||
margin-bottom: 40px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background-color: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #ecf5ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.feature-icon .icon {
|
||||
font-size: 40px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 算法区域 */
|
||||
.algorithms-section {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 32px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.view-all {
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.view-all:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.algorithms-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.algorithm-card {
|
||||
background-color: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.algorithm-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.algorithm-card h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.algorithm-type {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background-color: #ecf5ff;
|
||||
color: #409EFF;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.algorithm-desc {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.algorithm-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-btn,
|
||||
.call-btn {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.detail-btn:hover {
|
||||
background-color: #e4e7ed;
|
||||
}
|
||||
|
||||
.call-btn {
|
||||
background-color: #409EFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.call-btn:hover {
|
||||
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 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.hero-buttons {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.hero-buttons a {
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.features-grid,
|
||||
.algorithms-grid,
|
||||
.tech-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
151
frontend/src/views/LoginView.vue
Normal file
151
frontend/src/views/LoginView.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-form-wrapper">
|
||||
<h1>登录</h1>
|
||||
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" class="login-form">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="loginForm.username" placeholder="请输入用户名" prefix-icon="UserFilled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="login-button" :loading="loading" @click="handleLogin">登录</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item class="register-link">
|
||||
<span>还没有账号?</span>
|
||||
<router-link to="/register">立即注册</router-link>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { UserFilled, Lock } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 获取路由和存储
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 表单状态
|
||||
const loginFormRef = ref()
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单验证规则
|
||||
const loginRules = ref({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
// 验证表单(使用Promise形式)
|
||||
await loginFormRef.value.validate()
|
||||
|
||||
// 验证成功,开始登录
|
||||
loading.value = true
|
||||
|
||||
// 调用登录方法
|
||||
const success = await userStore.login(loginForm.value)
|
||||
|
||||
if (success) {
|
||||
// 登录成功,显示成功提示
|
||||
ElMessage.success('登录成功')
|
||||
// 跳转到首页
|
||||
router.push('/')
|
||||
} else {
|
||||
// 登录失败,显示错误信息
|
||||
if (userStore.error) {
|
||||
ElMessage.error(userStore.error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败或其他错误
|
||||
console.error('登录失败:', error)
|
||||
// 表单验证失败时,Element Plus会自动显示错误信息
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f7fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-form-wrapper {
|
||||
background-color: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-form-wrapper h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.register-link span {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.register-link a {
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.register-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.login-form-wrapper {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
frontend/src/views/NotFoundView.vue
Normal file
49
frontend/src/views/NotFoundView.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="not-found">
|
||||
<h1>404</h1>
|
||||
<h2>页面不存在</h2>
|
||||
<p>抱歉,您访问的页面不存在或已被移除。</p>
|
||||
<el-button type="primary" @click="goHome">返回首页</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.not-found {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.not-found h1 {
|
||||
font-size: 8rem;
|
||||
font-weight: 700;
|
||||
color: #409EFF;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.not-found h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.not-found p {
|
||||
font-size: 1.2rem;
|
||||
color: #606266;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
183
frontend/src/views/RegisterView.vue
Normal file
183
frontend/src/views/RegisterView.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="register-container">
|
||||
<div class="register-form-wrapper">
|
||||
<h1>注册</h1>
|
||||
<el-form :model="registerForm" :rules="registerRules" ref="registerFormRef" class="register-form">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="registerForm.username" placeholder="请输入用户名" prefix-icon="UserFilled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="registerForm.email" type="email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码" prefix-icon="Lock" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="请确认密码" prefix-icon="Lock" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="register-button" :loading="loading" @click="handleRegister">注册</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item class="login-link">
|
||||
<span>已有账号?</span>
|
||||
<router-link to="/login">立即登录</router-link>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { UserFilled, Lock } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 获取路由和存储
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 表单状态
|
||||
const registerFormRef = ref()
|
||||
const registerForm = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 表单验证规则
|
||||
const registerRules = ref({
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度应在3-20之间', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度至少为6位', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (value !== registerForm.value.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 处理注册
|
||||
const handleRegister = async () => {
|
||||
if (!registerFormRef.value) return
|
||||
|
||||
// 验证表单
|
||||
await registerFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 调用注册方法
|
||||
const success = await userStore.register({
|
||||
username: registerForm.value.username,
|
||||
email: registerForm.value.email,
|
||||
password: registerForm.value.password,
|
||||
role: 'user' // 默认角色
|
||||
})
|
||||
|
||||
if (success) {
|
||||
// 注册成功,显示成功提示
|
||||
ElMessage.success('注册成功,请登录')
|
||||
// 跳转到登录页
|
||||
router.push('/login')
|
||||
} else {
|
||||
// 注册失败,显示错误信息
|
||||
if (userStore.error) {
|
||||
ElMessage.error(userStore.error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error)
|
||||
ElMessage.error('注册失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.register-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f7fa;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.register-form-wrapper {
|
||||
background-color: #fff;
|
||||
padding: 40px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.register-form-wrapper h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.register-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.register-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-link span {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.login-link a {
|
||||
color: #409EFF;
|
||||
text-decoration: none;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.login-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.register-form-wrapper {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
361
frontend/src/views/ResultDisplayView.vue
Normal file
361
frontend/src/views/ResultDisplayView.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div class="result-display-view">
|
||||
<el-breadcrumb separator="/" class="breadcrumb">
|
||||
<el-breadcrumb-item><router-link to="/">首页</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item><router-link to="/algorithms">算法列表</router-link></el-breadcrumb-item>
|
||||
<el-breadcrumb-item>结果展示</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
|
||||
<el-loading v-if="loading" :fullscreen="true" text="加载中..." />
|
||||
|
||||
<div v-else class="content-container">
|
||||
<el-card class="control-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>结果展示与分析</h2>
|
||||
<el-button type="primary" @click="refreshData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="filters" inline>
|
||||
<el-form-item label="算法类型">
|
||||
<el-select v-model="filters.algorithmType" placeholder="选择算法类型">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="计算机视觉" value="computer_vision" />
|
||||
<el-option label="自然语言处理" value="nlp" />
|
||||
<el-option label="机器学习" value="ml" />
|
||||
<el-option label="强化学习" value="reinforcement_learning" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="时间范围">
|
||||
<el-date-picker
|
||||
v-model="filters.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="applyFilters">应用筛选</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<el-row :gutter="20" class="stats-row">
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.totalCalls }}</div>
|
||||
<div class="stat-label">总调用次数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.successRate }}%</div>
|
||||
<div class="stat-label">成功率</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.avgResponseTime }}s</div>
|
||||
<div class="stat-label">平均响应时间</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.uniqueAlgorithms }}</div>
|
||||
<div class="stat-label">算法种类</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 结果可视化 -->
|
||||
<result-visualization
|
||||
:result-data="selectedResult"
|
||||
:enable-comparison="true"
|
||||
:algorithm-type="selectedResult?.algorithm_type || 'general'"
|
||||
/>
|
||||
|
||||
<!-- 调用历史列表 -->
|
||||
<el-card class="history-card">
|
||||
<template #header>
|
||||
<h3>调用历史</h3>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
:data="callHistory"
|
||||
style="width: 100%"
|
||||
@row-click="selectResult"
|
||||
row-key="id"
|
||||
:highlight-current-row="true"
|
||||
>
|
||||
<el-table-column prop="id" label="调用ID" width="200" />
|
||||
<el-table-column prop="algorithm_name" label="算法名称" width="150" />
|
||||
<el-table-column prop="version" label="版本" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)">{{ row.status }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="response_time" label="响应时间(s)" width="120" />
|
||||
<el-table-column prop="created_at" label="调用时间" width="180" />
|
||||
<el-table-column label="操作" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click.stop="viewResultDetails(row)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="pagination.currentPage"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-size="pagination.pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import ResultVisualization from '../components/core/ResultVisualization.vue'
|
||||
import { useAlgorithmStore } from '../stores/algorithm'
|
||||
|
||||
// 状态
|
||||
const loading = ref(true)
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 过滤器
|
||||
const filters = reactive({
|
||||
algorithmType: '',
|
||||
dateRange: []
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalCalls: 0,
|
||||
successRate: 0,
|
||||
avgResponseTime: 0,
|
||||
uniqueAlgorithms: 0
|
||||
})
|
||||
|
||||
// 调用历史
|
||||
const callHistory = ref<any[]>([])
|
||||
const selectedResult = ref<any>(null)
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 获取调用历史
|
||||
await fetchCallHistory()
|
||||
|
||||
// 获取统计数据
|
||||
await fetchStats()
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCallHistory = async () => {
|
||||
// 模拟获取调用历史数据
|
||||
// 在实际应用中,这里会调用API获取数据
|
||||
callHistory.value = [
|
||||
{
|
||||
id: 'call-001',
|
||||
algorithm_name: '图像分类算法',
|
||||
version: 'v1.0',
|
||||
status: 'success',
|
||||
response_time: 1.2,
|
||||
created_at: '2023-12-01 10:30:00',
|
||||
algorithm_type: 'computer_vision',
|
||||
result_data: {
|
||||
predictions: [
|
||||
{ class: 'cat', confidence: 0.95 },
|
||||
{ class: 'dog', confidence: 0.05 }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'call-002',
|
||||
algorithm_name: '文本情感分析',
|
||||
version: 'v2.1',
|
||||
status: 'success',
|
||||
response_time: 0.8,
|
||||
created_at: '2023-12-01 11:15:00',
|
||||
algorithm_type: 'nlp',
|
||||
result_data: {
|
||||
sentiment: 'positive',
|
||||
confidence: 0.89,
|
||||
keywords: ['happy', 'excited', 'good']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'call-003',
|
||||
algorithm_name: '回归预测模型',
|
||||
version: 'v1.2',
|
||||
status: 'failed',
|
||||
response_time: null,
|
||||
created_at: '2023-12-01 12:00:00',
|
||||
algorithm_type: 'ml',
|
||||
result_data: { error: 'Model not converged' }
|
||||
}
|
||||
]
|
||||
|
||||
if (callHistory.value.length > 0) {
|
||||
selectedResult.value = callHistory.value[0].result_data
|
||||
}
|
||||
|
||||
pagination.total = callHistory.value.length
|
||||
}
|
||||
|
||||
const fetchStats = async () => {
|
||||
// 模拟获取统计数据
|
||||
// 在实际应用中,这里会调用API获取统计数据
|
||||
stats.totalCalls = 156
|
||||
stats.successRate = 92.3
|
||||
stats.avgResponseTime = 1.45
|
||||
stats.uniqueAlgorithms = 12
|
||||
}
|
||||
|
||||
const getStatusTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return 'success'
|
||||
case 'failed': return 'danger'
|
||||
case 'pending': return 'warning'
|
||||
case 'running': return 'info'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const selectResult = (row: any) => {
|
||||
selectedResult.value = row.result_data
|
||||
}
|
||||
|
||||
const viewResultDetails = (row: any) => {
|
||||
// 查看结果详情
|
||||
console.log('Viewing details for:', row)
|
||||
selectResult(row)
|
||||
}
|
||||
|
||||
const applyFilters = () => {
|
||||
// 应用筛选条件
|
||||
loadData()
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.pageSize = val
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.currentPage = val
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-display-view {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.control-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.history-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</file_path>
|
||||
451
frontend/src/views/admin/AdminAlgorithmServicesView.vue
Normal file
451
frontend/src/views/admin/AdminAlgorithmServicesView.vue
Normal file
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<div class="admin-services-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>算法服务管理</h1>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="refreshServices">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新服务列表
|
||||
</el-button>
|
||||
<el-button type="success" @click="navigateToServiceRegistration">
|
||||
<el-icon><Plus /></el-icon>
|
||||
注册新服务
|
||||
</el-button>
|
||||
<el-button type="info" @click="showServiceDetailDialog = false">
|
||||
<el-icon><View /></el-icon>
|
||||
查看服务详情
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 服务状态统计 -->
|
||||
<div class="status-stats">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ totalServices }}</div>
|
||||
<div class="stat-label">总服务数</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ runningServices }}</div>
|
||||
<div class="stat-label">运行中</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ stoppedServices }}</div>
|
||||
<div class="stat-label">已停止</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ errorServices }}</div>
|
||||
<div class="stat-label">异常</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 服务列表 -->
|
||||
<el-table :data="services" style="width: 100%" @row-click="handleRowClick">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="name" label="服务名称" width="200" />
|
||||
<el-table-column prop="service_id" label="服务ID" width="200" />
|
||||
<el-table-column prop="algorithm_name" label="算法名称" width="150" />
|
||||
<el-table-column prop="version" label="版本" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="getStatusType(scope.row.status)"
|
||||
>
|
||||
{{ scope.row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.last_heartbeat) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="startService(scope.row)" v-if="scope.row.status === 'stopped'">启动</el-button>
|
||||
<el-button size="small" type="warning" @click="stopService(scope.row)" v-else-if="scope.row.status === 'running'">停止</el-button>
|
||||
<el-button size="small" @click="restartService(scope.row)">重启</el-button>
|
||||
<el-button size="small" type="primary" @click="viewServiceDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 服务详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="showServiceDetailDialog"
|
||||
title="服务详情"
|
||||
width="80%"
|
||||
>
|
||||
<div v-if="selectedService" class="service-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="服务名称">{{ selectedService.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务ID">{{ selectedService.service_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="算法名称">{{ selectedService.algorithm_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="版本">{{ selectedService.version }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ selectedService.status }}</el-descriptions-item>
|
||||
<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="启动时间">{{ 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>
|
||||
|
||||
<!-- 服务配置 -->
|
||||
<div class="service-config">
|
||||
<h3>服务配置</h3>
|
||||
<el-card>
|
||||
<pre>{{ JSON.stringify(selectedService.config, null, 2) }}</pre>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 服务日志 -->
|
||||
<div class="service-logs">
|
||||
<h3>服务日志</h3>
|
||||
<el-card>
|
||||
<div class="logs-container">
|
||||
<div v-for="(log, index) in selectedService.logs" :key="index" class="log-item">
|
||||
{{ log }}
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-empty description="请选择一个服务查看详情" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showServiceDetailDialog = 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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 状态管理
|
||||
const services = ref<any[]>([])
|
||||
const showServiceDetailDialog = ref(false)
|
||||
const selectedService = ref<any>(null)
|
||||
|
||||
// 计算服务统计信息
|
||||
const totalServices = computed(() => services.value.length)
|
||||
const runningServices = computed(() => services.value.filter(s => s.status === 'running').length)
|
||||
const stoppedServices = computed(() => services.value.filter(s => s.status === 'stopped').length)
|
||||
const errorServices = computed(() => services.value.filter(s => s.status === 'error').length)
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return 'success'
|
||||
case 'stopped':
|
||||
return 'info'
|
||||
case 'error':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return '-'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
// 加载服务列表
|
||||
const loadServices = async () => {
|
||||
try {
|
||||
// 这里应该调用后端API获取服务列表
|
||||
// 暂时使用模拟数据
|
||||
services.value = [
|
||||
{
|
||||
id: '1',
|
||||
service_id: 'service-001',
|
||||
name: '图像分类服务',
|
||||
algorithm_name: 'image-classification',
|
||||
version: '1.0.0',
|
||||
status: 'running',
|
||||
host: '192.168.1.100',
|
||||
port: 8000,
|
||||
api_url: 'http://192.168.1.100:8000/execute',
|
||||
start_time: new Date().toISOString(),
|
||||
last_heartbeat: new Date().toISOString(),
|
||||
description: '基于ResNet的图像分类服务',
|
||||
config: {
|
||||
cpu_limit: '2核',
|
||||
memory_limit: '4GB',
|
||||
replicas: 2,
|
||||
timeout: 30
|
||||
},
|
||||
logs: [
|
||||
'[2024-01-01 10:00:00] 服务启动成功',
|
||||
'[2024-01-01 10:05:00] 注册到服务中心',
|
||||
'[2024-01-01 10:10:00] 处理请求: 图像分类',
|
||||
'[2024-01-01 10:15:00] 请求处理完成,耗时: 120ms'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
service_id: 'service-002',
|
||||
name: '文本分类服务',
|
||||
algorithm_name: 'text-classification',
|
||||
version: '1.0.0',
|
||||
status: 'stopped',
|
||||
host: '192.168.1.101',
|
||||
port: 8001,
|
||||
api_url: 'http://192.168.1.101:8001/execute',
|
||||
start_time: new Date().toISOString(),
|
||||
last_heartbeat: new Date().toISOString(),
|
||||
description: '基于BERT的文本分类服务',
|
||||
config: {
|
||||
cpu_limit: '4核',
|
||||
memory_limit: '8GB',
|
||||
replicas: 1,
|
||||
timeout: 60
|
||||
},
|
||||
logs: [
|
||||
'[2024-01-01 09:00:00] 服务启动成功',
|
||||
'[2024-01-01 09:30:00] 服务停止'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
service_id: 'service-003',
|
||||
name: '目标检测服务',
|
||||
algorithm_name: 'object-detection',
|
||||
version: '2.0.0',
|
||||
status: 'running',
|
||||
host: '192.168.1.102',
|
||||
port: 8002,
|
||||
api_url: 'http://192.168.1.102:8002/execute',
|
||||
start_time: new Date().toISOString(),
|
||||
last_heartbeat: new Date().toISOString(),
|
||||
description: '基于YOLOv5的目标检测服务',
|
||||
config: {
|
||||
cpu_limit: '8核',
|
||||
memory_limit: '16GB',
|
||||
replicas: 1,
|
||||
timeout: 120
|
||||
},
|
||||
logs: [
|
||||
'[2024-01-01 11:00:00] 服务启动成功',
|
||||
'[2024-01-01 11:05:00] 注册到服务中心',
|
||||
'[2024-01-01 11:10:00] 处理请求: 目标检测',
|
||||
'[2024-01-01 11:15:00] 请求处理完成,耗时: 500ms'
|
||||
]
|
||||
}
|
||||
]
|
||||
console.log('服务列表加载完成')
|
||||
} catch (error) {
|
||||
console.error('加载服务列表失败:', error)
|
||||
ElMessage.error('加载服务列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新服务列表
|
||||
const refreshServices = async () => {
|
||||
await loadServices()
|
||||
ElMessage.success('服务列表已刷新')
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
const startService = async (service: any) => {
|
||||
try {
|
||||
console.log('启动服务:', service.name)
|
||||
// 这里应该调用后端API启动服务
|
||||
|
||||
// 模拟启动服务
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 更新服务状态
|
||||
service.status = 'running'
|
||||
ElMessage.success('服务启动成功')
|
||||
} catch (error) {
|
||||
console.error('启动服务失败:', error)
|
||||
ElMessage.error('启动服务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 停止服务
|
||||
const stopService = async (service: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要停止服务 ${service.name} 吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
console.log('停止服务:', service.name)
|
||||
// 这里应该调用后端API停止服务
|
||||
|
||||
// 模拟停止服务
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 更新服务状态
|
||||
service.status = 'stopped'
|
||||
ElMessage.success('服务停止成功')
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
}
|
||||
|
||||
// 重启服务
|
||||
const restartService = async (service: any) => {
|
||||
try {
|
||||
console.log('重启服务:', service.name)
|
||||
// 这里应该调用后端API重启服务
|
||||
|
||||
// 模拟重启服务
|
||||
service.status = 'restarting'
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
service.status = 'running'
|
||||
|
||||
ElMessage.success('服务重启成功')
|
||||
} catch (error) {
|
||||
console.error('重启服务失败:', error)
|
||||
ElMessage.error('重启服务失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 查看服务详情
|
||||
const viewServiceDetail = (service: any) => {
|
||||
selectedService.value = service
|
||||
showServiceDetailDialog.value = true
|
||||
}
|
||||
|
||||
// 处理行点击
|
||||
const handleRowClick = (row: any) => {
|
||||
selectedService.value = row
|
||||
}
|
||||
|
||||
// 导航到服务注册页面
|
||||
const navigateToServiceRegistration = () => {
|
||||
router.push('/admin/service-registration')
|
||||
}
|
||||
|
||||
// 组件挂载时加载服务列表
|
||||
onMounted(async () => {
|
||||
await loadServices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-services-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-stats {
|
||||
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;
|
||||
}
|
||||
|
||||
.service-detail {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.service-config,
|
||||
.service-logs {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.service-config h3,
|
||||
.service-logs h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.log-item:nth-child(odd) {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.status-stats {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-services-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.status-stats {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1381
frontend/src/views/admin/AdminAlgorithmsView.vue
Normal file
1381
frontend/src/views/admin/AdminAlgorithmsView.vue
Normal file
File diff suppressed because it is too large
Load Diff
174
frontend/src/views/admin/AdminApiKeysView.vue
Normal file
174
frontend/src/views/admin/AdminApiKeysView.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="admin-api-keys">
|
||||
<h2>API密钥管理</h2>
|
||||
<div class="api-keys-actions">
|
||||
<el-button type="primary" @click="showCreateDialog">创建API密钥</el-button>
|
||||
</div>
|
||||
<el-table :data="apiKeys" style="width: 100%" class="api-keys-table">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="密钥名称" />
|
||||
<el-table-column prop="key" label="API密钥" show-overflow-tooltip />
|
||||
<el-table-column prop="user_id" label="用户ID" width="100" />
|
||||
<el-table-column prop="is_active" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.is_active ? 'success' : 'danger'">{{ scope.row.is_active ? '活跃' : '已停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expires_at" label="过期时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.expires_at) || '永不过期' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="toggleApiKey(scope.row.id, !scope.row.is_active)">{{ scope.row.is_active ? '停用' : '激活' }}</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteApiKey(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 创建API密钥对话框 -->
|
||||
<el-dialog v-model="createDialogVisible" title="创建API密钥">
|
||||
<el-form :model="createForm" label-width="100px">
|
||||
<el-form-item label="密钥名称" required>
|
||||
<el-input v-model="createForm.name" placeholder="请输入密钥名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户ID" required>
|
||||
<el-input v-model="createForm.user_id" type="number" placeholder="请输入用户ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间">
|
||||
<el-date-picker
|
||||
v-model="createForm.expires_at"
|
||||
type="datetime"
|
||||
placeholder="选择过期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="createApiKey">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const apiKeys = ref<any[]>([
|
||||
{
|
||||
id: 1,
|
||||
user_id: 1,
|
||||
key: 'sk-1234567890abcdef1234567890abcdef',
|
||||
name: '管理员密钥',
|
||||
expires_at: null,
|
||||
is_active: true,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
])
|
||||
|
||||
const createDialogVisible = ref(false)
|
||||
|
||||
const createForm = ref({
|
||||
name: '',
|
||||
user_id: 1,
|
||||
expires_at: null
|
||||
})
|
||||
|
||||
const formatDate = (dateString?: string | null) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const showCreateDialog = () => {
|
||||
createForm.value = {
|
||||
name: '',
|
||||
user_id: 1,
|
||||
expires_at: null
|
||||
}
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
const createApiKey = async () => {
|
||||
// 这里应该调用后端API创建API密钥
|
||||
// 暂时只做前端模拟
|
||||
const newApiKey = {
|
||||
id: Date.now(),
|
||||
user_id: createForm.value.user_id,
|
||||
key: `sk-${Math.random().toString(36).substring(2, 42)}`,
|
||||
name: createForm.value.name,
|
||||
expires_at: createForm.value.expires_at,
|
||||
is_active: true,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
apiKeys.value.push(newApiKey)
|
||||
createDialogVisible.value = false
|
||||
}
|
||||
|
||||
const toggleApiKey = async (id: number, isActive: boolean) => {
|
||||
// 这里应该调用后端API更新API密钥状态
|
||||
// 暂时只做前端模拟
|
||||
const index = apiKeys.value.findIndex(key => key.id === id)
|
||||
if (index !== -1) {
|
||||
apiKeys.value[index].is_active = isActive
|
||||
apiKeys.value[index].updated_at = new Date().toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteApiKey = async (id: number) => {
|
||||
// 这里应该调用后端API删除API密钥
|
||||
// 暂时只做前端模拟
|
||||
apiKeys.value = apiKeys.value.filter(key => key.id !== id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-api-keys {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.admin-api-keys h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.api-keys-actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.api-keys-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-api-keys {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.api-keys-actions {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.api-keys-table {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
263
frontend/src/views/admin/AdminApiManagementView.vue
Normal file
263
frontend/src/views/admin/AdminApiManagementView.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div class="admin-api-management-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>API管理</h1>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="showAddDialog = true">
|
||||
<el-icon><plus /></el-icon>
|
||||
封装新API
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- API列表 -->
|
||||
<el-table :data="apis" style="width: 100%">
|
||||
<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="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="created_at" label="创建时间" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="editApi(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteApi(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 封装API对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
title="封装新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="选择算法" prop="algorithm_id">
|
||||
<el-select v-model="apiForm.algorithm_id" placeholder="选择算法">
|
||||
<el-option
|
||||
v-for="algorithm in algorithms"
|
||||
:key="algorithm.id"
|
||||
:label="algorithm.name"
|
||||
:value="algorithm.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择版本" prop="version_id">
|
||||
<el-select v-model="apiForm.version_id" placeholder="选择版本">
|
||||
<el-option
|
||||
v-for="version in versions"
|
||||
:key="version.id"
|
||||
:label="version.version"
|
||||
:value="version.id"
|
||||
/>
|
||||
</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>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="apiForm.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入API描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showAddDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="addApi">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
|
||||
// 获取路由和存储
|
||||
const router = useRouter()
|
||||
const algorithmStore = useAlgorithmStore()
|
||||
|
||||
// 状态管理
|
||||
const apis = ref([])
|
||||
const algorithms = ref([])
|
||||
const versions = ref([])
|
||||
const showAddDialog = ref(false)
|
||||
const apiFormRef = ref()
|
||||
const apiForm = ref({
|
||||
name: '',
|
||||
algorithm_id: '',
|
||||
version_id: '',
|
||||
path: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const apiRules = ref({
|
||||
name: [
|
||||
{ required: true, message: '请输入API名称', 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) => {
|
||||
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()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 加载算法列表
|
||||
const loadAlgorithms = async () => {
|
||||
await algorithmStore.fetchAlgorithms()
|
||||
algorithms.value = algorithmStore.algorithms
|
||||
}
|
||||
|
||||
// 加载版本列表
|
||||
const loadVersions = async (algorithmId: string) => {
|
||||
if (algorithmId) {
|
||||
const algorithm = algorithmStore.algorithms.find(a => a.id === algorithmId)
|
||||
if (algorithm && algorithm.versions) {
|
||||
versions.value = algorithm.versions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听算法选择变化
|
||||
const handleAlgorithmChange = (algorithmId: string) => {
|
||||
loadVersions(algorithmId)
|
||||
}
|
||||
|
||||
// 添加API
|
||||
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: ''
|
||||
}
|
||||
|
||||
// 重新加载API列表
|
||||
await loadApis()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 编辑API
|
||||
const editApi = (api: any) => {
|
||||
// 这里应该打开编辑对话框,暂时打印
|
||||
console.log('编辑API:', api)
|
||||
}
|
||||
|
||||
// 删除API
|
||||
const deleteApi = (apiId: string) => {
|
||||
// 这里应该调用后端API删除API,暂时打印
|
||||
console.log('删除API:', apiId)
|
||||
ElMessage.success('API删除成功')
|
||||
// 重新加载API列表
|
||||
loadApis()
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
onMounted(async () => {
|
||||
await loadAlgorithms()
|
||||
await loadApis()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-api-management-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.admin-api-management-container h1 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-table-column {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
617
frontend/src/views/admin/AdminServiceRegistrationView.vue
Normal file
617
frontend/src/views/admin/AdminServiceRegistrationView.vue
Normal file
@@ -0,0 +1,617 @@
|
||||
<template>
|
||||
<div class="admin-service-registration-container">
|
||||
<!-- 页面标题 -->
|
||||
<h1>服务注册</h1>
|
||||
|
||||
<!-- 操作说明 -->
|
||||
<el-card class="info-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>操作说明</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="info-content">
|
||||
<p>1. 选择要注册为服务的算法仓库</p>
|
||||
<p>2. 选择仓库中的算法</p>
|
||||
<p>3. 配置服务参数</p>
|
||||
<p>4. 提交注册请求</p>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<el-card class="registration-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>服务注册表单</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
:model="serviceForm"
|
||||
:rules="rules"
|
||||
ref="serviceFormRef"
|
||||
label-width="120px"
|
||||
class="service-form"
|
||||
>
|
||||
<!-- 仓库选择 -->
|
||||
<el-form-item label="算法仓库" prop="repository_id">
|
||||
<el-select
|
||||
v-model="serviceForm.repository_id"
|
||||
placeholder="请选择算法仓库"
|
||||
@change="onRepositoryChange"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="repo in repositories"
|
||||
:key="repo.id"
|
||||
:label="repo.name"
|
||||
:value="repo.id"
|
||||
>
|
||||
<div class="option-content">
|
||||
<div class="option-name">{{ repo.name }}</div>
|
||||
<div class="option-desc">{{ repo.description || '无描述' }}</div>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 算法选择 -->
|
||||
<el-form-item label="算法" prop="algorithm_id">
|
||||
<el-select
|
||||
v-model="serviceForm.algorithm_id"
|
||||
placeholder="请选择算法"
|
||||
@change="onAlgorithmChange"
|
||||
class="w-full"
|
||||
:disabled="!serviceForm.repository_id"
|
||||
>
|
||||
<el-option
|
||||
v-for="algorithm in algorithms"
|
||||
:key="algorithm.id"
|
||||
:label="algorithm.name"
|
||||
:value="algorithm.id"
|
||||
>
|
||||
<div class="option-content">
|
||||
<div class="option-name">{{ algorithm.name }}</div>
|
||||
<div class="option-desc">{{ algorithm.description || '无描述' }}</div>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 服务基本信息 -->
|
||||
<el-form-item label="服务名称" prop="name">
|
||||
<el-input
|
||||
v-model="serviceForm.name"
|
||||
placeholder="请输入服务名称"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="服务版本" prop="version">
|
||||
<el-input
|
||||
v-model="serviceForm.version"
|
||||
placeholder="请输入服务版本"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="服务描述" prop="description">
|
||||
<el-input
|
||||
v-model="serviceForm.description"
|
||||
type="textarea"
|
||||
placeholder="请输入服务描述"
|
||||
rows="3"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 服务配置 -->
|
||||
<el-form-item label="服务类型" prop="service_type">
|
||||
<el-select
|
||||
v-model="serviceForm.service_type"
|
||||
placeholder="请选择服务类型"
|
||||
class="w-full"
|
||||
>
|
||||
<el-option label="HTTP REST API" value="http" />
|
||||
<el-option label="gRPC" value="grpc" />
|
||||
<el-option label="消息队列" value="mq" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="主机地址" prop="host">
|
||||
<el-input
|
||||
v-model="serviceForm.host"
|
||||
placeholder="请输入主机地址"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="服务端口" prop="port">
|
||||
<el-input-number
|
||||
v-model="serviceForm.port"
|
||||
:min="1"
|
||||
:max="65535"
|
||||
:step="1"
|
||||
class="w-full"
|
||||
placeholder="请输入服务端口"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="超时时间" prop="timeout">
|
||||
<el-input-number
|
||||
v-model="serviceForm.timeout"
|
||||
:min="1"
|
||||
:max="300"
|
||||
:step="1"
|
||||
class="w-full"
|
||||
placeholder="请输入超时时间(秒)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 高级配置 -->
|
||||
<el-form-item label="高级配置">
|
||||
<el-collapse>
|
||||
<el-collapse-item title="资源配置">
|
||||
<el-form-item label="CPU限制">
|
||||
<el-input
|
||||
v-model="serviceForm.cpu_limit"
|
||||
placeholder="例如:2核"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="内存限制">
|
||||
<el-input
|
||||
v-model="serviceForm.memory_limit"
|
||||
placeholder="例如:4GB"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="副本数">
|
||||
<el-input-number
|
||||
v-model="serviceForm.replicas"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="1"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="健康检查">
|
||||
<el-form-item label="健康检查路径">
|
||||
<el-input
|
||||
v-model="serviceForm.health_check_path"
|
||||
placeholder="例如:/health"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="健康检查间隔">
|
||||
<el-input-number
|
||||
v-model="serviceForm.health_check_interval"
|
||||
:min="1"
|
||||
:max="60"
|
||||
:step="1"
|
||||
class="w-full"
|
||||
placeholder="请输入健康检查间隔(秒)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<el-form-item>
|
||||
<div class="form-actions">
|
||||
<el-button type="primary" @click="submitForm" :loading="submitLoading">
|
||||
<el-icon><Check /></el-icon>
|
||||
提交注册
|
||||
</el-button>
|
||||
<el-button @click="resetForm">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 注册结果对话框 -->
|
||||
<el-dialog
|
||||
v-model="showResultDialog"
|
||||
:title="registrationResult.success ? '注册成功' : '注册失败'"
|
||||
width="60%"
|
||||
>
|
||||
<div v-if="registrationResult.success" class="result-success">
|
||||
<el-alert
|
||||
:title="'服务注册成功!'"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-descriptions :column="2" border style="margin-top: 20px;">
|
||||
<el-descriptions-item label="服务名称">{{ registrationResult.service?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务ID">{{ registrationResult.service?.service_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="服务状态">{{ registrationResult.service?.status }}</el-descriptions-item>
|
||||
<el-descriptions-item label="API地址">{{ registrationResult.service?.api_url }}</el-descriptions-item>
|
||||
<el-descriptions-item label="主机" :span="2">{{ registrationResult.service?.host }}:{{ registrationResult.service?.port }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div v-else class="result-error">
|
||||
<el-alert
|
||||
:title="'服务注册失败!'"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<el-card style="margin-top: 20px;">
|
||||
<pre>{{ registrationResult.error }}</pre>
|
||||
</el-card>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showResultDialog = false">关闭</el-button>
|
||||
<el-button
|
||||
v-if="registrationResult.success"
|
||||
type="primary"
|
||||
@click="navigateToServiceManagement"
|
||||
>
|
||||
查看服务管理
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Check, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 表单引用
|
||||
const serviceFormRef = ref()
|
||||
|
||||
// 加载状态
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 仓库和算法列表
|
||||
const repositories = ref<any[]>([])
|
||||
const algorithms = ref<any[]>([])
|
||||
|
||||
// 服务表单
|
||||
const serviceForm = reactive({
|
||||
repository_id: '',
|
||||
algorithm_id: '',
|
||||
name: '',
|
||||
version: '1.0.0',
|
||||
description: '',
|
||||
service_type: 'http',
|
||||
host: '0.0.0.0',
|
||||
port: 8000,
|
||||
timeout: 30,
|
||||
cpu_limit: '2核',
|
||||
memory_limit: '4GB',
|
||||
replicas: 1,
|
||||
health_check_path: '/health',
|
||||
health_check_interval: 30
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
repository_id: [
|
||||
{ required: true, message: '请选择算法仓库', trigger: 'blur' }
|
||||
],
|
||||
algorithm_id: [
|
||||
{ required: true, message: '请选择算法', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入服务名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '服务名称长度应在 2-50 个字符之间', trigger: 'blur' }
|
||||
],
|
||||
version: [
|
||||
{ required: true, message: '请输入服务版本', trigger: 'blur' }
|
||||
],
|
||||
service_type: [
|
||||
{ required: true, message: '请选择服务类型', trigger: 'blur' }
|
||||
],
|
||||
host: [
|
||||
{ required: true, message: '请输入主机地址', trigger: 'blur' }
|
||||
],
|
||||
port: [
|
||||
{ required: true, message: '请输入服务端口', trigger: 'blur' }
|
||||
],
|
||||
timeout: [
|
||||
{ required: true, message: '请输入超时时间', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 注册结果
|
||||
const showResultDialog = ref(false)
|
||||
const registrationResult = reactive({
|
||||
success: false,
|
||||
service: null,
|
||||
error: ''
|
||||
})
|
||||
|
||||
// 加载仓库列表
|
||||
const loadRepositories = async () => {
|
||||
try {
|
||||
// 这里应该调用后端API获取仓库列表
|
||||
// 暂时使用模拟数据
|
||||
repositories.value = [
|
||||
{
|
||||
id: 'repo-001',
|
||||
name: '图像分类算法',
|
||||
description: '基于ResNet的图像分类算法仓库',
|
||||
type: 'python',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'repo-002',
|
||||
name: '文本分类算法',
|
||||
description: '基于BERT的文本分类算法仓库',
|
||||
type: 'python',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'repo-003',
|
||||
name: '目标检测算法',
|
||||
description: '基于YOLOv5的目标检测算法仓库',
|
||||
type: 'python',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'repo-004',
|
||||
name: '推荐系统算法',
|
||||
description: '基于协同过滤的推荐系统算法仓库',
|
||||
type: 'nodejs',
|
||||
status: 'active'
|
||||
}
|
||||
]
|
||||
console.log('仓库列表加载完成')
|
||||
} catch (error) {
|
||||
console.error('加载仓库列表失败:', error)
|
||||
ElMessage.error('加载仓库列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载算法列表
|
||||
const loadAlgorithms = async (repositoryId: string) => {
|
||||
try {
|
||||
// 这里应该根据仓库ID调用后端API获取算法列表
|
||||
// 暂时使用模拟数据
|
||||
algorithms.value = [
|
||||
{
|
||||
id: 'algo-001',
|
||||
name: 'ResNet图像分类',
|
||||
description: '使用ResNet50模型进行图像分类',
|
||||
repository_id: repositoryId,
|
||||
entry_point: 'model/predict.py'
|
||||
},
|
||||
{
|
||||
id: 'algo-002',
|
||||
name: 'MobileNet轻量分类',
|
||||
description: '使用MobileNet进行轻量级图像分类',
|
||||
repository_id: repositoryId,
|
||||
entry_point: 'model/mobilenet_predict.py'
|
||||
}
|
||||
]
|
||||
console.log('算法列表加载完成')
|
||||
} catch (error) {
|
||||
console.error('加载算法列表失败:', error)
|
||||
ElMessage.error('加载算法列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 仓库选择变化
|
||||
const onRepositoryChange = (repositoryId: string) => {
|
||||
if (repositoryId) {
|
||||
algorithms.value = []
|
||||
serviceForm.algorithm_id = ''
|
||||
loadAlgorithms(repositoryId)
|
||||
}
|
||||
}
|
||||
|
||||
// 算法选择变化
|
||||
const onAlgorithmChange = (algorithmId: string) => {
|
||||
if (algorithmId) {
|
||||
// 可以根据算法ID自动填充一些默认值
|
||||
const selectedAlgorithm = algorithms.value.find(a => a.id === algorithmId)
|
||||
if (selectedAlgorithm) {
|
||||
// 自动生成服务名称
|
||||
if (!serviceForm.name) {
|
||||
serviceForm.name = `${selectedAlgorithm.name}服务`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
if (!serviceFormRef.value) return
|
||||
|
||||
try {
|
||||
await serviceFormRef.value.validate()
|
||||
|
||||
submitLoading.value = true
|
||||
|
||||
// 构建服务配置
|
||||
const serviceConfig = {
|
||||
name: serviceForm.name,
|
||||
version: serviceForm.version,
|
||||
description: serviceForm.description,
|
||||
service_type: serviceForm.service_type,
|
||||
host: serviceForm.host,
|
||||
port: serviceForm.port,
|
||||
timeout: serviceForm.timeout,
|
||||
cpu_limit: serviceForm.cpu_limit,
|
||||
memory_limit: serviceForm.memory_limit,
|
||||
replicas: serviceForm.replicas,
|
||||
health_check_path: serviceForm.health_check_path,
|
||||
health_check_interval: serviceForm.health_check_interval
|
||||
}
|
||||
|
||||
console.log('提交服务注册请求:', {
|
||||
repository_id: serviceForm.repository_id,
|
||||
algorithm_id: serviceForm.algorithm_id,
|
||||
service_config: serviceConfig
|
||||
})
|
||||
|
||||
// 这里应该调用后端API注册服务
|
||||
// 暂时使用模拟数据
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
// 模拟注册结果
|
||||
const mockService = {
|
||||
service_id: `service-${Date.now()}`,
|
||||
name: serviceForm.name,
|
||||
version: serviceForm.version,
|
||||
description: serviceForm.description,
|
||||
status: 'running',
|
||||
host: serviceForm.host,
|
||||
port: serviceForm.port,
|
||||
api_url: `http://${serviceForm.host}:${serviceForm.port}`,
|
||||
algorithm_id: serviceForm.algorithm_id,
|
||||
repository_id: serviceForm.repository_id,
|
||||
created_at: new Date().toISOString(),
|
||||
last_heartbeat: new Date().toISOString()
|
||||
}
|
||||
|
||||
// 更新注册结果
|
||||
registrationResult.success = true
|
||||
registrationResult.service = mockService
|
||||
registrationResult.error = ''
|
||||
showResultDialog.value = true
|
||||
|
||||
ElMessage.success('服务注册成功')
|
||||
|
||||
// 重置表单
|
||||
resetForm()
|
||||
} catch (error: any) {
|
||||
console.error('服务注册失败:', error)
|
||||
registrationResult.success = false
|
||||
registrationResult.service = null
|
||||
registrationResult.error = error.message || '服务注册失败,请稍后重试'
|
||||
showResultDialog.value = true
|
||||
ElMessage.error('服务注册失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
if (serviceFormRef.value) {
|
||||
serviceFormRef.value.resetFields()
|
||||
}
|
||||
// 重置算法列表
|
||||
algorithms.value = []
|
||||
// 重置默认值
|
||||
serviceForm.version = '1.0.0'
|
||||
serviceForm.service_type = 'http'
|
||||
serviceForm.host = '0.0.0.0'
|
||||
serviceForm.port = 8000
|
||||
serviceForm.timeout = 30
|
||||
serviceForm.cpu_limit = '2核'
|
||||
serviceForm.memory_limit = '4GB'
|
||||
serviceForm.replicas = 1
|
||||
serviceForm.health_check_path = '/health'
|
||||
serviceForm.health_check_interval = 30
|
||||
}
|
||||
|
||||
// 跳转到服务管理页面
|
||||
const navigateToServiceManagement = () => {
|
||||
showResultDialog.value = false
|
||||
router.push('/admin/services')
|
||||
}
|
||||
|
||||
// 组件挂载时加载仓库列表
|
||||
onMounted(async () => {
|
||||
await loadRepositories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-service-registration-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.registration-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.service-form {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.option-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.option-desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-success {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.result-error {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.admin-service-registration-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.service-form {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-actions .el-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
241
frontend/src/views/admin/AdminUsersView.vue
Normal file
241
frontend/src/views/admin/AdminUsersView.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div class="admin-users">
|
||||
<h2>用户管理</h2>
|
||||
<div class="users-actions">
|
||||
<el-button type="primary" @click="showCreateDialog">创建用户</el-button>
|
||||
</div>
|
||||
<el-table :data="users" style="width: 100%" class="users-table">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" />
|
||||
<el-table-column prop="email" label="邮箱" />
|
||||
<el-table-column prop="role" label="角色" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getRoleType(scope.row.role)">{{ scope.row.role }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="showEditDialog(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteUser(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 创建用户对话框 -->
|
||||
<el-dialog v-model="createDialogVisible" title="创建用户">
|
||||
<el-form :model="createForm" label-width="100px">
|
||||
<el-form-item label="用户名" required>
|
||||
<el-input v-model="createForm.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" required>
|
||||
<el-input v-model="createForm.email" type="email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" required>
|
||||
<el-input v-model="createForm.password" type="password" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" required>
|
||||
<el-select v-model="createForm.role" placeholder="请选择角色">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="用户" value="user" />
|
||||
<el-option label="访客" value="guest" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="createUser">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑用户对话框 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑用户">
|
||||
<el-form :model="editForm" label-width="100px">
|
||||
<el-form-item label="用户名" required>
|
||||
<el-input v-model="editForm.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" required>
|
||||
<el-input v-model="editForm.email" type="email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="editForm.password" type="password" placeholder="请输入密码(留空表示不修改)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" required>
|
||||
<el-select v-model="editForm.role" placeholder="请选择角色">
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="用户" value="user" />
|
||||
<el-option label="访客" value="guest" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="updateUser">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const users = ref<any[]>([
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'admin@example.com',
|
||||
role: 'admin',
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user1',
|
||||
email: 'user1@example.com',
|
||||
role: 'user',
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
])
|
||||
|
||||
const createDialogVisible = ref(false)
|
||||
const editDialogVisible = ref(false)
|
||||
|
||||
const createForm = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
})
|
||||
|
||||
const editForm = ref({
|
||||
id: 0,
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
})
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const getRoleType = (role?: string) => {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return 'danger'
|
||||
case 'user':
|
||||
return 'primary'
|
||||
case 'guest':
|
||||
return 'info'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const showCreateDialog = () => {
|
||||
createForm.value = {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
}
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
const showEditDialog = (user: any) => {
|
||||
editForm.value = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
password: '',
|
||||
role: user.role
|
||||
}
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
const createUser = async () => {
|
||||
// 这里应该调用后端API创建用户
|
||||
// 暂时只做前端模拟
|
||||
const newUser = {
|
||||
id: Date.now(),
|
||||
...createForm.value,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
users.value.push(newUser)
|
||||
createDialogVisible.value = false
|
||||
}
|
||||
|
||||
const updateUser = async () => {
|
||||
// 这里应该调用后端API更新用户
|
||||
// 暂时只做前端模拟
|
||||
const index = users.value.findIndex(u => u.id === editForm.value.id)
|
||||
if (index !== -1) {
|
||||
const updateData = { ...editForm.value }
|
||||
if (!updateData.password) {
|
||||
delete updateData.password
|
||||
}
|
||||
users.value[index] = {
|
||||
...users.value[index],
|
||||
...updateData,
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
editDialogVisible.value = false
|
||||
}
|
||||
|
||||
const deleteUser = async (id: number) => {
|
||||
// 这里应该调用后端API删除用户
|
||||
// 暂时只做前端模拟
|
||||
users.value = users.value.filter(u => u.id !== id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-users {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.admin-users h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.users-actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-users {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.users-actions {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user