Files
algorithm/frontend/src/views/admin/AdminAlgorithmsView.vue
2026-02-08 14:42:58 +08:00

1382 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="admin-algorithms-container">
<!-- 页面标题 -->
<h1>算法仓库管理</h1>
<!-- 操作栏 -->
<div class="action-bar">
<el-button type="primary" @click="openAddRepoDialog">
<el-icon><plus /></el-icon>
添加算法仓库
</el-button>
<el-button type="info" @click="showGiteaConfigDialog = true">
<el-icon><Setting /></el-icon>
Gitea配置
</el-button>
</div>
<!-- 仓库列表 -->
<el-table :data="repos" style="width: 100%">
<el-table-column prop="name" label="仓库名称" width="200" />
<el-table-column prop="description" label="描述" />
<el-table-column prop="repo_url" label="仓库地址" />
<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="280">
<template #default="scope">
<el-button size="small" @click="editRepo(scope.row)">编辑</el-button>
<el-button size="small" type="primary" @click="viewRepo(scope.row)">查看仓库</el-button>
<el-button size="small" type="danger" @click="deleteRepo(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- Gitea配置对话框 -->
<el-dialog
v-model="showGiteaConfigDialog"
:title="isEditingGiteaConfig ? '编辑Gitea配置' : 'Gitea配置'"
width="50%"
>
<div class="gitea-config-header" v-if="!isEditingGiteaConfig">
<el-button type="primary" @click="isEditingGiteaConfig = true">
<el-icon><Edit /></el-icon>
编辑配置
</el-button>
</div>
<el-form :model="giteaConfigForm" :rules="giteaConfigRules" ref="giteaConfigFormRef">
<el-form-item label="Gitea服务器URL" prop="serverUrl">
<el-input
v-model="giteaConfigForm.serverUrl"
placeholder="请输入Gitea服务器URL例如 https://gitea.example.com"
:disabled="!isEditingGiteaConfig"
/>
</el-form-item>
<el-form-item label="访问令牌" prop="accessToken">
<el-input
v-model="giteaConfigForm.accessToken"
type="password"
placeholder="请输入Gitea访问令牌"
:disabled="!isEditingGiteaConfig"
/>
</el-form-item>
<el-form-item label="默认组织/用户" prop="defaultOwner">
<el-input
v-model="giteaConfigForm.defaultOwner"
placeholder="请输入默认组织或用户名"
:disabled="!isEditingGiteaConfig"
/>
</el-form-item>
<el-form-item label="仓库前缀" prop="repoPrefix">
<el-input
v-model="giteaConfigForm.repoPrefix"
placeholder="请输入仓库前缀,可选"
:disabled="!isEditingGiteaConfig"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelEditGiteaConfig">取消</el-button>
<el-button type="primary" @click="saveGiteaConfig" v-if="isEditingGiteaConfig">保存配置</el-button>
</span>
</template>
</el-dialog>
<!-- 添加算法仓库对话框 -->
<el-dialog
v-model="showAddRepoDialog"
title="添加算法仓库"
width="60%"
>
<el-form :model="repoForm" :rules="repoRules" ref="repoFormRef">
<el-form-item label="仓库名称" prop="name">
<el-input v-model="repoForm.name" placeholder="请输入仓库名称" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="repoForm.description"
type="textarea"
:rows="3"
placeholder="请输入仓库描述"
/>
</el-form-item>
<el-form-item label="仓库类型" prop="type">
<el-select v-model="repoForm.type" placeholder="选择仓库类型">
<el-option label="代码仓库" value="code" />
<el-option label="模型仓库" value="model" />
<el-option label="混合仓库" value="hybrid" />
</el-select>
</el-form-item>
<el-form-item label="Gitea服务器URL" prop="serverUrl">
<el-input v-model="repoForm.serverUrl" placeholder="Gitea服务器URL" readonly :disabled="true" />
</el-form-item>
<el-form-item label="Git仓库名" prop="gitRepoName">
<el-input v-model="repoForm.gitRepoName" placeholder="请输入Git仓库名" />
</el-form-item>
<el-form-item label="仓库地址" prop="repo_url">
<el-input v-model="repoForm.repo_url" placeholder="完整仓库地址" readonly />
</el-form-item>
<el-form-item label="分支" prop="branch">
<el-input v-model="repoForm.branch" placeholder="请输入仓库分支,默认为 main" />
</el-form-item>
<el-form-item label="本地路径" prop="local_path">
<el-input v-model="repoForm.local_path" placeholder="请选择本地存储路径" readonly>
<template #append>
<el-button @click="selectLocalPath">
<el-icon><Folder /></el-icon>
选择文件夹
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<input
ref="folderInput"
type="file"
webkitdirectory
multiple
style="display: none"
@change="handleFolderSelect"
/>
<template #footer>
<span class="dialog-footer">
<el-button @click="showAddRepoDialog = false">取消</el-button>
<el-button type="primary" @click="addRepo">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 编辑仓库对话框 -->
<el-dialog
v-model="showEditRepoDialog"
title="编辑算法仓库"
width="60%"
>
<el-form :model="editRepoForm" :rules="repoRules" ref="editRepoFormRef">
<el-form-item label="仓库名称" prop="name">
<el-input v-model="editRepoForm.name" placeholder="请输入仓库名称" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="editRepoForm.description"
type="textarea"
:rows="3"
placeholder="请输入仓库描述"
/>
</el-form-item>
<el-form-item label="仓库类型" prop="type">
<el-select v-model="editRepoForm.type" placeholder="选择仓库类型">
<el-option label="代码仓库" value="code" />
<el-option label="模型仓库" value="model" />
<el-option label="混合仓库" value="hybrid" />
</el-select>
</el-form-item>
<el-form-item label="Gitea服务器URL" prop="serverUrl">
<el-input v-model="editRepoForm.serverUrl" placeholder="Gitea服务器URL" readonly :disabled="true" />
</el-form-item>
<el-form-item label="Git仓库名" prop="gitRepoName">
<el-input v-model="editRepoForm.gitRepoName" placeholder="请输入Git仓库名" readonly disabled />
</el-form-item>
<el-form-item label="仓库地址" prop="repo_url">
<el-input v-model="editRepoForm.repo_url" placeholder="完整仓库地址" readonly disabled />
</el-form-item>
<el-form-item label="分支" prop="branch">
<el-input v-model="editRepoForm.branch" placeholder="请输入仓库分支,默认为 main" readonly disabled />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showEditRepoDialog = false">取消</el-button>
<el-button type="primary" @click="updateRepo">保存</el-button>
</span>
</template>
</el-dialog>
<!-- 上传进度对话框 -->
<el-dialog
v-model="isUploading"
title="上传代码到Gitea"
width="40%"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
>
<div class="upload-progress-container">
<el-progress :percentage="uploadProgress" :status="uploadProgress === 100 ? 'success' : ''" />
<div class="upload-progress-text" v-if="uploadProgress < 100">
正在上传代码到Gitea仓库请稍候...
</div>
<div class="upload-progress-text" v-else>
代码上传完成
</div>
</div>
</el-dialog>
<!-- 文件选择器 - 移到主页面中确保始终可用 -->
<input
ref="folderInput"
type="file"
webkitdirectory
multiple
style="display: none"
@change="handleFolderSelect"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { Plus, Setting, ArrowDown, UploadFilled, Edit, Folder } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 获取路由
const router = useRouter()
// 状态管理
const repos = ref<any[]>([])
const showAddRepoDialog = ref(false)
const showEditRepoDialog = ref(false)
const showGiteaConfigDialog = ref(false)
const isEditingGiteaConfig = ref(false)
const repoFormRef = ref<any>()
const editRepoFormRef = ref<any>()
const giteaConfigFormRef = ref<any>() // 部署状态
const folderInput = ref<HTMLInputElement>()
const selectedFiles = ref<File[]>([])
const currentEditingRepo = ref<any>(null)
// 打开添加仓库对话框
const openAddRepoDialog = () => {
// 填充 Gitea 服务器 URL
repoForm.value.serverUrl = giteaConfigForm.value.serverUrl
// 重置其他字段
repoForm.value.gitRepoName = ''
repoForm.value.repo_url = ''
repoForm.value.local_path = ''
// 重置选中的文件
selectedFiles.value = []
// 显示对话框
showAddRepoDialog.value = true
}
// 选择本地路径
const selectLocalPath = () => {
if (folderInput.value) {
folderInput.value.click()
}
}
// 处理文件夹选择
const handleFolderSelect = (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files && target.files.length > 0) {
// Chrome 浏览器对 webkitdirectory 有 1000 个文件的限制
// 如果文件数量正好是 1000可能是被浏览器截断了
const CHROME_FILE_LIMIT = 1000
if (target.files.length >= CHROME_FILE_LIMIT) {
ElMessage.error(
`错误:检测到 ${target.files.length} 个文件。\n\n` +
`Chrome 浏览器对文件夹选择有 ${CHROME_FILE_LIMIT} 个文件的限制。\n\n` +
`解决方案:\n` +
`1. 排除不需要的目录(如 node_modules, .git, dist 等)\n` +
`2. 将文件分成多个文件夹分批上传\n` +
`3. 使用命令行工具git直接推送代码到 Gitea`
)
return // 阻止继续
}
// 检查文件数量并给出警告(但不禁用功能)
if (target.files.length > 500) {
ElMessage.warning(`检测到较多文件 (${target.files.length} 个),处理可能需要较长时间,请耐心等待。`)
}
// 清空之前选中的文件
selectedFiles.value = []
// 定义需要排除的文件和目录模式
const excludePatterns = [
// 编译输出目录
/node_modules[\/\\]/,
/dist[\/\\]/,
/build[\/\\]/,
/target[\/\\]/,
/out[\/\\]/,
/\.next[\/\\]/,
/\.nuxt[\/\\]/,
// 缓存和临时文件目录
/__pycache__[\/\\]/,
/\.pytest_cache[\/\\]/,
/\.cache[\/\\]/,
/\.temp[\/\\]/,
/\.tmp[\/\\]/,
// IDE 和编辑器目录
/\.idea[\/\\]/,
/\.vscode[\/\\]/,
/\.vs[\/\\]/,
// 版本控制目录
/\.git[\/\\]/,
/\.svn[\/\\]/,
/\.hg[\/\\]/,
// 日志文件
/\.log$/,
// 操作系统文件
/\.DS_Store$/,
/Thumbs\.db$/,
/desktop\.ini$/,
// 依赖锁文件(可选)
/package-lock\.json$/,
/yarn\.lock$/,
/pnpm-lock\.yaml$/,
]
// 保存选中的文件到数组并过滤排除的文件
const excludedFiles: string[] = []
for (let i = 0; i < target.files.length; i++) {
const file = target.files[i] as File;
// 确保我们保留相对路径信息
(file as any).relativePath = (file as any).webkitRelativePath || file.name;
// 检查是否应该排除
const relativePath = (file as any).relativePath || file.name
const shouldExclude = excludePatterns.some(pattern => pattern.test(relativePath))
if (shouldExclude) {
excludedFiles.push(relativePath)
} else {
selectedFiles.value.push(file)
}
}
// 报告排除的文件
if (excludedFiles.length > 0) {
console.log(`自动排除了 ${excludedFiles.length} 个编译/缓存文件`)
// 按目录分组统计
const excludedDirs: { [key: string]: number } = {}
excludedFiles.forEach(path => {
const dir = path.split('/')[0]
excludedDirs[dir] = (excludedDirs[dir] || 0) + 1
})
const excludeSummary = Object.entries(excludedDirs)
.map(([dir, count]) => `${dir}: ${count}`)
.join(', ')
ElMessage.success(
`已自动排除 ${excludedFiles.length} 个编译/缓存文件 (${excludeSummary})`
)
}
console.log('Selected files array:', selectedFiles.value)
console.log('Number of files selected:', selectedFiles.value.length)
// 显示所有选中的文件
console.log('All selected files:')
for (let i = 0; i < selectedFiles.value.length; i++) {
const file = selectedFiles.value[i]
console.log(`File ${i + 1}: ${file.name}`)
console.log(` Relative Path: ${(file as any).relativePath}`)
console.log(` Size: ${file.size} bytes`)
console.log(` Type: ${file.type}`)
}
// 获取第一个文件的路径,然后提取文件夹路径
const firstFile = selectedFiles.value[0]
if (firstFile) {
console.log('First file details:')
console.log(` Name: ${firstFile.name}`)
console.log(` Relative Path: ${(firstFile as any).relativePath}`)
const relativePath = (firstFile as any).relativePath
if (relativePath && relativePath.includes('/')) {
// 提取文件夹路径(移除文件名)
const folderPath = relativePath.substring(0, relativePath.lastIndexOf('/'))
repoForm.value.local_path = folderPath
console.log('Selected folder:', folderPath)
} else {
// 如果没有相对路径,则使用文件名作为路径
repoForm.value.local_path = firstFile.name
console.log('Selected file path:', firstFile.name)
}
}
// 重置输入,以便可以选择相同的文件夹
// 注意:现在使用数组存储文件,所以重置输入不会丢失选中的文件
if (folderInput.value) {
console.log('Resetting file input to allow re-selection')
folderInput.value.value = ''
}
// 检查是否有文件被选择
if (selectedFiles.value.length > 0) {
console.log('Files selected:', selectedFiles.value.length)
}
} else {
console.log('No files selected')
selectedFiles.value = []
}
}
// 上传文件到服务器
const uploadFilesToServer = async (files: File[], algorithmId?: string) => {
try {
console.log('=== 开始上传文件 ===')
console.log('Files array:', files)
console.log('Number of files to upload:', files.length)
console.log('Algorithm ID:', algorithmId)
// 检查文件数量限制 (Chrome 浏览器限制为 1000)
const MAX_FILES_PER_BATCH = 1000 // Chrome 浏览器限制
if (files.length > MAX_FILES_PER_BATCH) {
console.log(`文件数量超过限制 (${MAX_FILES_PER_BATCH} 个)`)
ElMessage.error(
`文件数量超过限制 (${files.length} 个)\n\n` +
`Chrome 浏览器限制每次最多选择 ${MAX_FILES_PER_BATCH} 个文件。\n\n` +
`建议:\n` +
`1. 排除 node_modules, .git, dist 等目录\n` +
`2. 分批选择文件夹上传\n` +
`3. 使用 git 命令行直接推送`
)
return false
}
// 检查总文件大小
let totalSize = 0;
for (let i = 0; i < files.length; i++) {
totalSize += files[i].size;
}
console.log(`Total upload size: ${totalSize} bytes (${(totalSize / (1024 * 1024)).toFixed(2)} MB)`);
// 如果总大小超过一定限制,分批上传
const MAX_BATCH_SIZE = 50 * 1024 * 1024; // 50MB per batch
if (totalSize > MAX_BATCH_SIZE) {
console.log('文件过大,分批上传...');
return await uploadFilesInBatches(files, algorithmId, MAX_BATCH_SIZE);
}
const axios = await import('axios')
const formData = new FormData()
// 添加所有文件到 FormData
console.log('=== 构建FormData ===')
for (let i = 0; i < files.length; i++) {
const file = files[i]
// 使用保存的相对路径,而不是原始的 webkitRelativePath可能在某些浏览器中不可用
const filePath = (file as any).relativePath || file.name
console.log(`Adding file ${i + 1}/${files.length}: ${filePath}`)
console.log(` Size: ${file.size} bytes`)
console.log(` Type: ${file.type}`)
formData.append('files', file, filePath)
}
// 添加算法ID
const finalAlgorithmId = algorithmId || repoForm.value.gitRepoName
formData.append('algorithm_id', finalAlgorithmId)
console.log('Algorithm ID in form data:', finalAlgorithmId)
// 显示FormData内容仅用于调试
console.log('FormData keys:', Array.from(formData.keys()))
// 上传文件
console.log('=== 发送上传请求 ===')
console.log('Sending request to /api/gitea/repos/upload')
const response = await axios.default.post('/api/gitea/repos/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
// 监控上传进度
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
uploadProgress.value = progress
console.log(`Upload progress: ${progress}% (${progressEvent.loaded}/${progressEvent.total} bytes)`)
}
},
timeout: 600000 // 10分钟超时支持大文件上传
})
console.log('=== 上传响应 ===')
console.log('Status code:', response.status)
console.log('Response data:', response.data)
if (response.data.success) {
console.log('✅ 文件上传成功')
} else {
console.log('❌ 文件上传失败')
}
return response.data.success
} catch (error: any) {
console.error('=== 上传文件失败 ===')
console.error('Error:', error)
console.error('Error message:', error.message)
console.error('Error stack:', error.stack)
console.error('Response:', error.response)
console.error('Response data:', error.response?.data)
console.error('Response status:', error.response?.status)
// 检查是否是文件数量超过限制的错误
if (error.response?.data?.detail?.includes('Maximum number of files')) {
ElMessage.error(`文件数量超过限制,请减少文件数量后重试`)
} else {
ElMessage.error('文件上传失败,请检查网络连接或文件大小')
}
return false
}
}
// 分批上传文件
const uploadFilesInBatches = async (files: File[], algorithmId?: string, limit: number = 50 * 1024 * 1024) => {
console.log('开始分批上传文件...');
// 判断是基于文件数量还是文件大小分批
const MAX_FILES_PER_BATCH = 1000 // Chrome 浏览器限制为 1000 个文件
const isFileCountBatch = files.length > MAX_FILES_PER_BATCH
// 按批次分割文件
const batches: File[][] = [];
let currentBatch: File[] = [];
let currentBatchSize = 0;
for (const file of files) {
if (isFileCountBatch) {
// 基于文件数量分批 (Chrome 限制 1000)
if (currentBatch.length >= MAX_FILES_PER_BATCH) {
batches.push([...currentBatch]);
currentBatch = [file];
} else {
currentBatch.push(file);
}
} else {
// 基于文件大小分批
if (currentBatchSize + file.size > limit && currentBatch.length > 0) {
batches.push([...currentBatch]);
currentBatch = [file];
currentBatchSize = file.size;
} else {
currentBatch.push(file);
currentBatchSize += file.size;
}
}
}
// 添加最后一个批次
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
console.log(`总共 ${files.length} 个文件分为 ${batches.length}`);
console.log(`分批模式: ${isFileCountBatch ? '基于文件数量' : '基于文件大小'}`);
// 逐批上传
for (let i = 0; i < batches.length; i++) {
console.log(`上传第 ${i + 1} 批文件,包含 ${batches[i].length} 个文件`);
const batchResult = await uploadSingleBatch(batches[i], algorithmId);
if (!batchResult) {
console.error(`${i + 1} 批上传失败`);
return false;
}
// 更新进度
const progressPerBatch = Math.floor(100 / batches.length);
uploadProgress.value = Math.min(progressPerBatch * (i + 1), 90); // 最多到90%保留10%给最后的推送
}
console.log('所有批次上传完成');
return true;
};
// 上传单个批次
const uploadSingleBatch = async (batch: File[], algorithmId?: string) => {
try {
const axios = await import('axios');
const formData = new FormData();
for (let i = 0; i < batch.length; i++) {
const file = batch[i];
const filePath = (file as any).relativePath || file.name;
formData.append('files', file, filePath);
}
const finalAlgorithmId = algorithmId || repoForm.value.gitRepoName;
formData.append('algorithm_id', finalAlgorithmId);
console.log(`上传批次,包含 ${batch.length} 个文件`);
const response = await axios.default.post('/api/gitea/repos/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 600000 // 10分钟超时
});
return response.data.success;
} catch (error: any) {
console.error('批次上传失败:', error);
return false;
}
};
// 监听 Gitea 配置对话框的显示状态
watch(() => showGiteaConfigDialog.value, async (newValue) => {
if (newValue) {
// 打开对话框时,重置为非编辑模式并加载最新配置
isEditingGiteaConfig.value = false
await loadGiteaConfig()
}
})
// Gitea配置表单
const giteaConfigForm = ref({
serverUrl: '',
accessToken: '',
defaultOwner: '',
repoPrefix: ''
})
// Gitea配置表单验证规则
const giteaConfigRules = ref({
serverUrl: [
{ required: true, message: '请输入Gitea服务器URL', trigger: 'blur' }
],
accessToken: [
{ required: true, message: '请输入访问令牌', trigger: 'blur' }
],
defaultOwner: [
{ required: true, message: '请输入默认组织或用户名', trigger: 'blur' }
]
})
// 仓库表单
const repoForm = ref({
name: '',
description: '',
type: 'code',
serverUrl: '',
gitRepoName: '',
repo_url: '',
branch: 'main',
local_path: ''
})
// 编辑仓库表单
const editRepoForm = ref({
name: '',
description: '',
type: 'code',
serverUrl: '',
gitRepoName: '',
repo_url: '',
branch: 'main'
})
// 监听 Gitea 配置变化更新仓库表单中的服务器URL
watch(() => giteaConfigForm.value.serverUrl, (newValue) => {
repoForm.value.serverUrl = newValue
// 重新计算仓库地址
updateRepoUrl()
})
// 监听Git仓库名变化更新完整仓库地址
watch(() => repoForm.value.gitRepoName, () => {
updateRepoUrl()
})
// 更新仓库地址
const updateRepoUrl = () => {
if (repoForm.value.serverUrl && repoForm.value.gitRepoName) {
// 拼接完整仓库地址
const serverUrl = repoForm.value.serverUrl.endsWith('/') ? repoForm.value.serverUrl.slice(0, -1) : repoForm.value.serverUrl
// 考虑Gitea配置中的前缀
const repoPrefix = giteaConfigForm.value.repoPrefix || ''
const fullRepoName = repoPrefix + repoForm.value.gitRepoName
repoForm.value.repo_url = `${serverUrl}/${giteaConfigForm.value.defaultOwner}/${fullRepoName}.git`
} else {
repoForm.value.repo_url = ''
}
}
// 仓库表单验证规则
const repoRules = ref({
name: [
{ required: true, message: '请输入仓库名称', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入仓库描述', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择仓库类型', trigger: 'blur' }
],
gitRepoName: [
{ required: true, message: '请输入Git仓库名', trigger: 'blur' }
]
})
// 格式化日期
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleString()
}
// 加载仓库列表
const loadRepos = async () => {
try {
// 导入axios
const axios = await import('axios')
console.log('开始加载仓库列表...')
// 调用后端API获取仓库列表
const response = await axios.default.get('/api/repositories')
console.log('仓库列表API响应:', response)
console.log('响应状态:', response.status)
console.log('响应数据:', response.data)
console.log('响应数据类型:', typeof response.data)
console.log('success字段:', response.data.success)
console.log('repositories字段:', response.data.repositories)
if (response.data.success) {
repos.value = response.data.repositories
console.log('仓库列表加载完成,共', repos.value.length, '个仓库')
console.log('仓库列表:', repos.value)
} else {
console.error('加载仓库列表失败success为false')
ElMessage.error('加载仓库列表失败')
}
} catch (error) {
console.error('加载仓库列表失败:', error)
console.error('错误类型:', typeof error)
console.error('错误详情:', error)
ElMessage.error('加载仓库列表失败')
}
}
// 上传进度状态
const uploadProgress = ref(0)
const isUploading = ref(false)
// 添加仓库
const addRepo = async () => {
try {
// 验证表单
if (!repoForm.value.name || !repoForm.value.description || !repoForm.value.type || !repoForm.value.gitRepoName) {
ElMessage.error('请填写完整的仓库信息')
return
}
// 导入axios
const axios = await import('axios')
// 调用后端API添加仓库
const response = await axios.default.post('/api/repositories', {
name: repoForm.value.name,
description: repoForm.value.description,
type: repoForm.value.type,
repo_url: repoForm.value.repo_url,
branch: repoForm.value.branch,
local_path: repoForm.value.local_path
})
if (response.data.success) {
// 显示上传进度对话框
isUploading.value = true
uploadProgress.value = 0
try {
// 计算包含前缀的完整仓库名称
// 前端根据配置添加前缀,后端直接使用
const repoPrefix = giteaConfigForm.value.repoPrefix || ''
const fullRepoName = repoPrefix + repoForm.value.gitRepoName
// 首先在Gitea上创建仓库
const createResponse = await axios.default.post('/api/gitea/repos/create', {
algorithm_id: fullRepoName,
algorithm_name: repoForm.value.name,
description: repoForm.value.description
})
console.log('仓库创建响应:', createResponse.data)
// 然后克隆仓库(如果需要)
try {
const cloneResponse = await axios.default.post('/api/gitea/repos/clone', {
repo_url: repoForm.value.repo_url,
algorithm_id: fullRepoName,
branch: repoForm.value.branch
})
console.log('仓库克隆响应:', cloneResponse.data)
} catch (cloneError: any) {
console.log('仓库克隆可能不需要或失败,继续执行...', cloneError.message)
}
// 上传文件到服务器
console.log('Selected files before upload:', selectedFiles.value)
if (selectedFiles.value && selectedFiles.value.length > 0) {
console.log('Uploading files...')
const uploadSuccess = await uploadFilesToServer(selectedFiles.value, fullRepoName)
if (!uploadSuccess) {
ElMessage.warning('文件上传失败,但仓库创建成功')
} else {
console.log('Files uploaded successfully')
}
} else {
console.log('No files selected for upload')
}
// 最后推送代码到Gitea仓库
const pushResponse = await axios.default.post('/api/gitea/repos/push', {
algorithm_id: fullRepoName,
message: `Initial commit for ${repoForm.value.name}`
})
if (pushResponse.data.success) {
ElMessage.success('仓库添加成功代码已上传到Gitea')
} else {
ElMessage.warning('仓库添加成功,但代码上传失败')
console.error('推送失败响应:', pushResponse.data)
}
} catch (error: any) {
console.error('上传代码失败:', error)
console.error('错误详情:', error.response?.data || error.message)
ElMessage.warning('仓库添加成功,但代码上传失败')
} finally {
// 完成上传
uploadProgress.value = 100
setTimeout(() => {
isUploading.value = false
showAddRepoDialog.value = false
// 重新加载仓库列表
loadRepos()
// 重置表单
repoForm.value = {
name: '',
description: '',
type: 'code',
serverUrl: '',
gitRepoName: '',
repo_url: '',
branch: 'main',
local_path: ''
}
// 清空选中的文件
selectedFiles.value = []
}, 500)
}
} else {
ElMessage.error('添加仓库失败')
}
} catch (error: any) {
console.error('添加仓库失败:', error)
console.error('错误详情:', error.response?.data || error.message)
ElMessage.error('添加仓库失败')
}
}
// 编辑仓库
const editRepo = async (repo: any) => {
console.log('编辑仓库:', repo)
try {
// 导入axios
const axios = await import('axios')
// 调用后端API获取仓库详细信息
const response = await axios.default.get(`/api/repositories/${repo.id}`)
if (response.data.success) {
const repoDetails = response.data.repository
// 填充编辑表单
editRepoForm.value = {
name: repoDetails.name,
description: repoDetails.description,
type: repoDetails.type,
serverUrl: giteaConfigForm.value.serverUrl,
gitRepoName: extractRepoName(repoDetails.repo_url),
repo_url: repoDetails.repo_url,
branch: repoDetails.branch
}
currentEditingRepo.value = repoDetails
showEditRepoDialog.value = true
} else {
ElMessage.error('获取仓库信息失败')
}
} catch (error) {
console.error('获取仓库信息失败:', error)
ElMessage.error('获取仓库信息失败')
}
}
// 从仓库URL中提取仓库名称
const extractRepoName = (repoUrl: string) => {
if (!repoUrl) return ''
// 从类似 https://gitea.example.com/owner/repo.git 的URL中提取repo部分
const urlParts = repoUrl.split('/')
if (urlParts.length > 0) {
const lastPart = urlParts[urlParts.length - 1]
// 移除.git后缀
return lastPart.replace('.git', '')
}
return ''
}
// 更新仓库
const updateRepo = async () => {
try {
// 验证表单
if (!editRepoForm.value.name || !editRepoForm.value.description ||
!editRepoForm.value.type || !editRepoForm.value.gitRepoName) {
ElMessage.error('请填写完整的仓库信息')
return
}
// 导入axios
const axios = await import('axios')
console.log('正在更新仓库:', {
repoId: currentEditingRepo.value.id,
name: editRepoForm.value.name,
description: editRepoForm.value.description,
type: editRepoForm.value.type
})
// 调用后端API更新仓库 - 只更新名称、描述和类型
const response = await axios.default.put(`/api/repositories/${currentEditingRepo.value.id}`, {
name: editRepoForm.value.name,
description: editRepoForm.value.description,
type: editRepoForm.value.type
})
if (response.data.success) {
// 同时更新 Gitea 仓库信息
try {
// 直接使用gitRepoName作为algorithm_id因为它已经包含了前缀
// 从仓库URL中提取的gitRepoName已经是完整的仓库名称包含前缀
const algorithmId = editRepoForm.value.gitRepoName
console.log('更新Gitea仓库信息使用algorithm_id:', algorithmId)
// 调用 Gitea API 更新仓库信息 - 只更新描述,不更新名称
const giteaResponse = await axios.default.patch('/api/gitea/repos/update', {
algorithm_id: algorithmId,
description: editRepoForm.value.description,
private: false // 暂时默认为公开仓库,后续可以添加专门的私有仓库选项
})
console.log('发送到Gitea API的参数:', {
algorithm_id: algorithmId,
description: editRepoForm.value.description,
private: false
})
console.log('Gitea仓库更新响应:', giteaResponse.data)
// 显示Gitea更新成功的消息
if (giteaResponse.data.success) {
console.log('Gitea仓库信息更新成功')
} else {
console.log('Gitea仓库信息更新失败')
}
} catch (giteaError) {
console.error('更新Gitea仓库信息失败:', giteaError)
console.error('错误详情:', giteaError.response?.data || giteaError.message)
// Gitea更新失败不影响本地更新的成功状态
ElMessage.warning('本地仓库更新成功但Gitea仓库信息更新失败')
}
ElMessage.success('仓库更新成功')
showEditRepoDialog.value = false
currentEditingRepo.value = null
// 重新加载仓库列表
await loadRepos()
// 重置表单
editRepoForm.value = {
name: '',
description: '',
type: 'code',
serverUrl: '',
gitRepoName: '',
repo_url: '',
branch: 'main'
}
} else {
ElMessage.error('更新仓库失败')
}
} catch (error: any) {
console.error('更新仓库失败:', error)
console.error('错误详情:', error.response?.data || error.message)
ElMessage.error('更新仓库失败')
}
}
// 删除仓库
const deleteRepo = async (repoId: string) => {
try {
await ElMessageBox.confirm('确定要删除这个仓库吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// 导入axios
const axios = await import('axios')
// 调用后端API删除仓库
const response = await axios.default.delete(`/api/repositories/${repoId}`)
if (response.data.success) {
ElMessage.success('仓库删除成功')
await loadRepos()
} else {
ElMessage.error('删除仓库失败')
}
} catch (error) {
// 用户取消删除
}
}
// 查看仓库
const viewRepo = (repo: any) => {
console.log('查看仓库:', repo)
// 构建Gitea仓库URL
const serverUrl = giteaConfigForm.value.serverUrl
const owner = giteaConfigForm.value.defaultOwner
let repoName = ''
// 从repo_url中提取仓库名称
if (repo.repo_url) {
// 例如:从 https://gitea.example.com/owner/repo.git 中提取 repo
const urlParts = repo.repo_url.split('/')
if (urlParts.length > 0) {
const lastPart = urlParts[urlParts.length - 1]
repoName = lastPart.replace('.git', '')
}
}
if (serverUrl && owner && repoName) {
const repoUrl = `${serverUrl}/${owner}/${repoName}`
console.log('打开仓库URL:', repoUrl)
window.open(repoUrl, '_blank')
} else {
ElMessage.warning('无法打开仓库请检查Gitea配置')
console.error('缺少必要的配置信息:', { serverUrl, owner, repoName })
}
}
// 保存Gitea配置
const saveGiteaConfig = async () => {
try {
// 验证表单
if (!giteaConfigForm.value.serverUrl || !giteaConfigForm.value.accessToken || !giteaConfigForm.value.defaultOwner) {
ElMessage.error('请填写完整的Gitea配置信息')
return
}
// 导入axios
const axios = await import('axios')
// 调用后端API保存配置
const response = await axios.default.post('/api/gitea/config', {
server_url: giteaConfigForm.value.serverUrl,
access_token: giteaConfigForm.value.accessToken,
default_owner: giteaConfigForm.value.defaultOwner,
repo_prefix: giteaConfigForm.value.repoPrefix || ''
})
if (response.data.success) {
ElMessage.success('Gitea配置保存成功')
showGiteaConfigDialog.value = false
} else {
ElMessage.error('保存配置失败')
}
} catch (error) {
console.error('保存Gitea配置失败:', error)
ElMessage.error('保存Gitea配置失败')
}
}
// 创建Gitea仓库
const createGiteaRepo = (algorithm: any) => {
console.log('创建Gitea仓库:', algorithm)
// 这里需要实现创建仓库的逻辑
ElMessage.info('创建仓库功能开发中')
}
// 克隆Gitea仓库
const cloneGiteaRepo = (algorithm: any) => {
console.log('克隆Gitea仓库:', algorithm)
// 这里需要实现克隆仓库的逻辑
ElMessage.info('克隆仓库功能开发中')
}
// 推送代码到Gitea仓库
const pushToGiteaRepo = (algorithm: any) => {
console.log('推送代码到Gitea仓库:', algorithm)
// 这里需要实现推送代码的逻辑
ElMessage.info('推送代码功能开发中')
}
// 从Gitea仓库拉取代码
const pullFromGiteaRepo = (algorithm: any) => {
console.log('从Gitea仓库拉取代码:', algorithm)
// 这里需要实现拉取代码的逻辑
ElMessage.info('拉取代码功能开发中')
}
// 取消编辑Gitea配置
const cancelEditGiteaConfig = () => {
if (isEditingGiteaConfig.value) {
// 如果正在编辑,取消编辑模式并重新加载配置
isEditingGiteaConfig.value = false
loadGiteaConfig()
} else {
// 如果不在编辑模式,关闭对话框
showGiteaConfigDialog.value = false
}
}
// 加载已保存的Gitea配置
const loadGiteaConfig = async () => {
try {
// 导入axios
const axios = await import('axios')
const response = await axios.default.get('/api/gitea/config')
if (response.data) {
const config = response.data
giteaConfigForm.value = {
serverUrl: config.server_url || '',
accessToken: config.access_token || '',
defaultOwner: config.default_owner || '',
repoPrefix: config.repo_prefix || ''
}
}
} catch (error) {
console.error('加载Gitea配置失败:', error)
// 配置不存在时不显示错误
}
}
// 组件挂载时加载仓库列表和Gitea配置
onMounted(async () => {
await loadRepos()
await loadGiteaConfig()
})
</script>
<style scoped>
.admin-algorithms-container {
padding: 20px;
}
.action-bar {
margin-bottom: 20px;
}
/* 步骤样式 */
.step-content {
padding: 30px 0;
}
.step-content h3 {
margin-bottom: 20px;
color: #333;
}
/* 部署方式选择 */
.deployment-option {
width: 30%;
min-width: 300px;
margin-right: 20px;
}
.option-content {
display: flex;
align-items: center;
padding: 20px;
}
.option-icon {
font-size: 32px;
margin-right: 20px;
color: #409EFF;
}
.option-text {
flex: 1;
}
.option-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 5px;
}
.option-desc {
font-size: 14px;
color: #666;
}
/* 代码编辑器 */
.code-editor {
border: 1px solid #e4e7ed;
border-radius: 4px;
}
/* 模型上传 */
.model-uploader {
margin-bottom: 20px;
}
/* 部署结果 */
.deployment-result {
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}
.result-details {
margin-top: 20px;
}
.error-message {
margin-top: 20px;
padding: 10px;
background-color: #fef0f0;
border: 1px solid #fbc4c4;
border-radius: 4px;
color: #f56c6c;
}
/* 部署中状态 */
.deploying {
min-height: 400px;
display: flex;
align-items: flex-start;
justify-content: center;
}
.deploying-content {
width: 100%;
max-width: 800px;
}
/* 部署日志 */
.deployment-logs {
margin-top: 30px;
}
.deployment-logs h4 {
margin-bottom: 15px;
color: #333;
}
.logs-card {
max-height: 400px;
overflow: hidden;
}
.logs-container {
max-height: 350px;
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;
}
/* 跳过步骤 */
.skip-step {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
/* 对话框底部按钮 */
.dialog-footer {
display: flex;
justify-content: space-between;
width: 100%;
}
/* 上传进度对话框样式 */
.upload-progress-container {
padding: 20px;
}
.upload-progress-text {
margin-top: 20px;
text-align: center;
color: #666;
font-size: 14px;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.deployment-option {
width: 100%;
margin-right: 0;
margin-bottom: 10px;
}
}
@media (max-width: 768px) {
.admin-algorithms-container {
padding: 10px;
}
.step-content {
padding: 20px 0;
}
.option-content {
padding: 15px;
}
.option-icon {
font-size: 24px;
margin-right: 15px;
}
.upload-progress-container {
padding: 10px;
}
}
</style>