仓库模块完了
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user