1382 lines
42 KiB
Vue
1382 lines
42 KiB
Vue
<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>
|