first commit

This commit is contained in:
2026-02-08 14:42:58 +08:00
commit 20e1deae21
8197 changed files with 2264639 additions and 0 deletions

1
frontend/.env Normal file
View File

@@ -0,0 +1 @@
VITE_API_BASE_URL=http://localhost:8001/api

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
frontend/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

23
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
# 使用Node.js作为基础镜像
FROM node:18-alpine
# 设置工作目录
WORKDIR /app
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm install
# 复制源代码
COPY . .
# 构建前端应用
RUN npm run build
# 暴露端口
EXPOSE 3000
# 启动开发服务器
CMD ["npm", "run", "dev"]

29
frontend/Dockerfile.prod Normal file
View File

@@ -0,0 +1,29 @@
# 构建阶段
FROM crpi-x2l5uviq1k8hji3c.ap-northeast-1.personal.cr.aliyuncs.com/yipaidocker-images/linux_arm64_node:18-alpine AS builder
WORKDIR /app
# 复制package文件并安装依赖
COPY package*.json ./
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 运行阶段
FROM crpi-x2l5uviq1k8hji3c.ap-northeast-1.personal.cr.aliyuncs.com/yipaidocker-images/linux_amd64_nginx:alpine
# 复制构建产物到nginx目录
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,32 @@
# 构建阶段
FROM crpi-x2l5uviq1k8hji3c.ap-northeast-1.personal.cr.aliyuncs.com/yipaidocker-images/linux_arm64_node:18-alpine AS builder
# 设置npm镜像源
RUN npm config set registry http://host.docker.internal:4873/
WORKDIR /app
# 复制package文件并安装依赖
COPY package*.json ./
RUN npm install --legacy-peer-deps
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 运行阶段
FROM crpi-x2l5uviq1k8hji3c.ap-northeast-1.personal.cr.aliyuncs.com/yipaidocker-images/linux_amd64_nginx:alpine
# 复制构建产物到nginx目录
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

5
frontend/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

28
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,28 @@
events {
worker_connections 1024;
}
http {
upstream backend {
server backend:8000;
}
server {
listen 80;
# 前端静态文件
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
# API请求代理到后端
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

3448
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
frontend/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "algorithm-showcase-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@monaco-editor/loader": "^1.7.0",
"@monaco-editor/react": "^4.7.0",
"axios": "^1.7.3",
"echarts": "^5.5.1",
"element-plus": "^2.7.8",
"monaco-editor": "^0.55.1",
"pinia": "^2.1.7",
"three": "^0.166.1",
"vue": "^3.4.29",
"vue-router": "^4.3.3"
},
"devDependencies": {
"@types/three": "^0.166.0",
"@vitejs/plugin-vue": "^5.0.5",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"typescript": "^5.2.2",
"vite": "^5.3.1",
"vue-tsc": "^2.0.21"
}
}

1
frontend/public/vite.svg Normal file
View 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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

172
frontend/src/App.vue Normal file
View 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>

View 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

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

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

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

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

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

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

View 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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

View File

@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

25
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

60
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,60 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { URL } from 'url'
const __dirname = decodeURIComponent(new URL('.', import.meta.url).pathname);
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api/v1'),
timeout: 600000 // 10分钟超时
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
minify: 'terser',
sourcemap: false,
// 代码分割配置
rollupOptions: {
output: {
manualChunks: {
// 第三方库分割
vendor: ['vue', 'vue-router', 'pinia'],
// UI库分割
element: ['element-plus'],
// 图表库分割
charts: ['echarts'],
// 网络请求库分割
http: ['axios']
},
// 缓存策略
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
},
// 压缩配置
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
// 别名配置
resolve: {
alias: {
'@': resolve(process.cwd(), 'src')
}
}
})