仓库模块完了

This commit is contained in:
2026-02-08 20:06:35 +08:00
parent 20e1deae21
commit f145df4fa6
29 changed files with 1415 additions and 993 deletions

View File

@@ -77,14 +77,7 @@ const routes: Array<RouteRecordRaw> = [
title: '用户管理'
}
},
{
path: 'api-keys',
name: 'AdminApiKeys',
component: () => import('../views/admin/AdminApiKeysView.vue'),
meta: {
title: 'API密钥管理'
}
},
{
path: 'api',
name: 'AdminApiManagement',
@@ -157,7 +150,9 @@ router.beforeEach((to, _from, next) => {
if (to.meta.requiresAdmin) {
try {
const userObj = JSON.parse(user)
if (userObj.role !== 'admin') {
// 检查用户是否为管理员,支持多种格式
const isAdmin = userObj.role?.name === 'admin' || userObj.role_name === 'admin'
if (!isAdmin) {
next({ name: 'Home' })
return
}

View File

@@ -1,12 +1,20 @@
import { defineStore } from 'pinia'
import axios from 'axios'
// 定义角色类型
interface Role {
id: string
name: string
description: string
}
// 定义用户类型
interface User {
id: string
username: string
email: string
role: string
role_id: string
role?: Role
status: string
}
@@ -21,7 +29,7 @@ interface RegisterRequest {
username: string
password: string
email: string
role: string
role_id: string
}
// 定义用户存储
@@ -35,7 +43,7 @@ export const useUserStore = defineStore('user', {
getters: {
isLoggedIn: (state) => !!state.token,
isAdmin: (state) => state.user?.role === 'admin'
isAdmin: (state) => state.user?.role?.name === 'admin'
},
actions: {

View File

@@ -38,12 +38,7 @@
</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>
@@ -62,7 +57,7 @@
<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'
import { DataAnalysis, User, Link, Cpu } from '@element-plus/icons-vue'
// 获取路由和路由器
const route = useRoute()

View File

@@ -28,11 +28,12 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } 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'
import axios from 'axios'
// 获取路由和存储
const router = useRouter()
@@ -47,6 +48,10 @@ const registerForm = ref({
confirmPassword: ''
})
// 角色列表和默认角色ID
const roles = ref<any[]>([])
const defaultRoleId = ref('')
// 加载状态
const loading = ref(false)
@@ -79,6 +84,23 @@ const registerRules = ref({
]
})
// 获取角色列表
const fetchRoles = async () => {
try {
const response = await axios.get('/api/users/roles')
roles.value = response.data
// 找到user角色的ID
const userRole = roles.value.find(r => r.name === 'user')
if (userRole) {
defaultRoleId.value = userRole.id
}
} catch (error) {
console.error('获取角色列表失败:', error)
ElMessage.error('获取角色列表失败,请稍后重试')
}
}
// 处理注册
const handleRegister = async () => {
if (!registerFormRef.value) return
@@ -89,12 +111,18 @@ const handleRegister = async () => {
loading.value = true
try {
// 确保有默认角色ID
if (!defaultRoleId.value) {
ElMessage.error('获取角色信息失败,请刷新页面重试')
return
}
// 调用注册方法
const success = await userStore.register({
username: registerForm.value.username,
email: registerForm.value.email,
password: registerForm.value.password,
role: 'user' // 默认角色
role_id: defaultRoleId.value // 使用默认角色ID
})
if (success) {
@@ -117,6 +145,11 @@ const handleRegister = async () => {
}
})
}
// 组件挂载时获取角色列表
onMounted(() => {
fetchRoles()
})
</script>
<style scoped>

View File

@@ -1,174 +0,0 @@
<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

@@ -50,32 +50,31 @@
<div class="option-content">
<div class="option-name">{{ repo.name }}</div>
<div class="option-desc">{{ repo.description || '无描述' }}</div>
<div class="option-url">{{ repo.repo_url || '无仓库地址' }}</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"
<!-- 仓库信息 -->
<el-form-item label="仓库描述">
<el-input
v-model="serviceForm.repository_description"
type="textarea"
placeholder="仓库描述"
rows="2"
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>
:disabled="true"
/>
</el-form-item>
<el-form-item label="仓库地址">
<el-input
v-model="serviceForm.repository_url"
placeholder="仓库地址"
class="w-full"
:disabled="true"
/>
</el-form-item>
<!-- 服务基本信息 -->
@@ -271,6 +270,7 @@ 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'
import axios from 'axios'
// 路由
const router = useRouter()
@@ -281,14 +281,14 @@ const serviceFormRef = ref()
// 加载状态
const submitLoading = ref(false)
// 仓库和算法列表
// 仓库列表
const repositories = ref<any[]>([])
const algorithms = ref<any[]>([])
// 服务表单
const serviceForm = reactive({
repository_id: '',
algorithm_id: '',
repository_description: '',
repository_url: '',
name: '',
version: '1.0.0',
description: '',
@@ -308,9 +308,6 @@ 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' }
@@ -343,93 +340,60 @@ const registrationResult = reactive({
// 加载仓库列表
const loadRepositories = async () => {
try {
// 这里应该调用后端API获取仓库列表
// 暂时使用模拟数据
// 调用后端API获取仓库列表
const response = await axios.get('/api/repositories')
if (response.data.success) {
repositories.value = response.data.repositories
console.log('仓库列表加载完成:', repositories.value)
} else {
throw new Error('Failed to load repositories')
}
} catch (error) {
console.error('加载仓库列表失败:', error)
ElMessage.error('加载仓库列表失败')
// 使用备用模拟数据
repositories.value = [
{
id: 'repo-001',
name: '图像分类算法',
description: '基于ResNet的图像分类算法仓库',
type: 'python',
status: 'active'
status: 'active',
repo_url: 'https://github.com/example/image-classification'
},
{
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'
status: 'active',
repo_url: 'https://github.com/example/text-classification'
}
]
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 selectedRepo = repositories.value.find(r => r.id === repositoryId)
if (selectedRepo) {
// 填充仓库描述和地址
serviceForm.repository_description = selectedRepo.description || ''
serviceForm.repository_url = selectedRepo.repo_url || ''
// 自动填充服务描述
if (!serviceForm.description) {
serviceForm.description = selectedRepo.description || ''
}
console.log('从仓库中带出默认值:', selectedRepo)
}
} else {
// 清空仓库信息
serviceForm.repository_description = ''
serviceForm.repository_url = ''
}
}
@@ -460,29 +424,24 @@ const submitForm = async () => {
console.log('提交服务注册请求:', {
repository_id: serviceForm.repository_id,
algorithm_id: serviceForm.algorithm_id,
repository_description: serviceForm.repository_description,
repository_url: serviceForm.repository_url,
service_config: serviceConfig
})
// 这里应该调用后端API注册服务
// 暂时使用模拟数据
await new Promise(resolve => setTimeout(resolve, 3000))
// 模拟注册结果
const mockService = {
service_id: `service-${Date.now()}`,
// 调用后端API注册服务
const response = await axios.post('/api/services/register', {
repository_id: serviceForm.repository_id,
name: serviceForm.name,
version: serviceForm.version,
description: serviceForm.description,
status: 'running',
service_type: serviceForm.service_type,
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()
}
timeout: serviceForm.timeout,
health_check_path: serviceForm.health_check_path
})
const mockService = response.data.service
// 更新注册结果
registrationResult.success = true
@@ -511,8 +470,6 @@ const resetForm = () => {
if (serviceFormRef.value) {
serviceFormRef.value.resetFields()
}
// 重置算法列表
algorithms.value = []
// 重置默认值
serviceForm.version = '1.0.0'
serviceForm.service_type = 'http'
@@ -524,6 +481,8 @@ const resetForm = () => {
serviceForm.replicas = 1
serviceForm.health_check_path = '/health'
serviceForm.health_check_interval = 30
serviceForm.repository_description = ''
serviceForm.repository_url = ''
}
// 跳转到服务管理页面
@@ -582,6 +541,14 @@ onMounted(async () => {
line-height: 1.4;
}
.option-url {
font-size: 11px;
color: #666;
line-height: 1.3;
margin-top: 2px;
word-break: break-all;
}
.form-actions {
display: flex;
gap: 10px;

View File

@@ -10,7 +10,7 @@
<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>
<el-tag :type="getRoleType(scope.row.role?.name || scope.row.role)">{{ scope.row.role?.name || scope.row.role }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
@@ -38,11 +38,12 @@
<el-form-item label="密码" required>
<el-input v-model="createForm.password" type="password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="确认密码" required>
<el-input v-model="createForm.confirmPassword" 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 v-model="createForm.role_id" placeholder="请选择角色">
<el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id" />
</el-select>
</el-form-item>
</el-form>
@@ -66,11 +67,12 @@
<el-form-item label="密码">
<el-input v-model="editForm.password" type="password" placeholder="请输入密码(留空表示不修改)" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="editForm.confirmPassword" 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 v-model="editForm.role_id" placeholder="请选择角色">
<el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id" />
</el-select>
</el-form-item>
</el-form>
@@ -85,41 +87,32 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import axios from 'axios'
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 users = ref<any[]>([])
const loading = ref(false)
const error = ref('')
const createDialogVisible = ref(false)
const editDialogVisible = ref(false)
const roles = ref<any[]>([])
const createForm = ref({
username: '',
email: '',
password: '',
role: 'user'
confirmPassword: '',
role_id: ''
})
const editForm = ref({
id: 0,
id: '',
username: '',
email: '',
password: '',
role: 'user'
confirmPassword: '',
role_id: ''
})
const formatDate = (dateString?: string) => {
@@ -134,19 +127,110 @@ const getRoleType = (role?: string) => {
return 'danger'
case 'user':
return 'primary'
case 'guest':
return 'info'
default:
return 'default'
}
}
// 获取角色列表
const fetchRoles = async () => {
try {
const response = await axios.get('/api/users/roles')
roles.value = response.data
} catch (err) {
console.error('获取角色列表失败:', err)
}
}
// 获取用户列表
const fetchUsers = async () => {
loading.value = true
error.value = ''
try {
const response = await axios.get('/api/users/')
users.value = response.data.users
} catch (err) {
console.error('获取用户列表失败:', err)
error.value = '获取用户列表失败'
} finally {
loading.value = false
}
}
// 创建用户
const createUser = async () => {
// 验证密码一致性
if (createForm.value.password !== createForm.value.confirmPassword) {
error.value = '两次输入的密码不一致'
return
}
loading.value = true
error.value = ''
try {
// 移除confirmPassword字段因为后端API不需要
const { confirmPassword, ...userData } = createForm.value
await axios.post('/api/users/register', userData)
await fetchUsers()
createDialogVisible.value = false
} catch (err) {
console.error('创建用户失败:', err)
error.value = '创建用户失败'
} finally {
loading.value = false
}
}
// 更新用户
const updateUser = async () => {
// 验证密码一致性(如果密码不为空)
if (editForm.value.password !== '' && editForm.value.password !== editForm.value.confirmPassword) {
error.value = '两次输入的密码不一致'
return
}
loading.value = true
error.value = ''
try {
const updateData = { ...editForm.value }
// 移除不需要的字段
if (!updateData.password) {
delete updateData.password
}
delete updateData.confirmPassword
await axios.put(`/api/users/${editForm.value.id}`, updateData)
await fetchUsers()
editDialogVisible.value = false
} catch (err) {
console.error('更新用户失败:', err)
error.value = '更新用户失败'
} finally {
loading.value = false
}
}
// 删除用户
const deleteUser = async (id: string) => {
loading.value = true
error.value = ''
try {
await axios.delete(`/api/users/${id}`)
await fetchUsers()
} catch (err) {
console.error('删除用户失败:', err)
error.value = '删除用户失败'
} finally {
loading.value = false
}
}
const showCreateDialog = () => {
createForm.value = {
username: '',
email: '',
password: '',
role: 'user'
confirmPassword: '',
role_id: roles.value.length > 0 ? roles.value.find(r => r.name === 'user')?.id || roles.value[0].id : ''
}
createDialogVisible.value = true
}
@@ -157,47 +241,17 @@ const showEditDialog = (user: any) => {
username: user.username,
email: user.email,
password: '',
role: user.role
confirmPassword: '',
role_id: user.role_id || user.role?.id || ''
}
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)
}
// 组件挂载时获取用户列表和角色列表
onMounted(async () => {
await fetchRoles()
await fetchUsers()
})
</script>
<style scoped>

View File

@@ -11,13 +11,13 @@ export default defineConfig({
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api/v1'),
timeout: 600000 // 10分钟超时
}
}
'/api': {
target: 'http://localhost:8001',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api/v1'),
timeout: 600000 // 10分钟超时
}
}
},
build: {
outDir: 'dist',