diff --git a/backend/apps/views/build.py b/backend/apps/views/build.py
index 1cece72..1dfca71 100644
--- a/backend/apps/views/build.py
+++ b/backend/apps/views/build.py
@@ -382,6 +382,13 @@ class BuildTaskView(View):
'message': '任务名称、项目和环境不能为空'
})
+ # 检查任务名称是否已存在
+ if BuildTask.objects.filter(name=name).exists():
+ return JsonResponse({
+ 'code': 400,
+ 'message': f'任务名称 "{name}" 已存在,请使用其他名称'
+ })
+
# 验证参数配置格式
if parameters:
import re
@@ -649,6 +656,12 @@ class BuildTaskView(View):
# 更新其他字段
if 'name' in data:
+ # 检查任务名称是否已存在
+ if BuildTask.objects.filter(name=name).exclude(task_id=task_id).exists():
+ return JsonResponse({
+ 'code': 400,
+ 'message': f'任务名称 "{name}" 已存在,请使用其他名称'
+ })
task.name = name
if 'description' in data:
task.description = description
diff --git a/web/src/router/index.js b/web/src/router/index.js
index 262d1c6..e91f392 100644
--- a/web/src/router/index.js
+++ b/web/src/router/index.js
@@ -89,6 +89,12 @@ const routes = [
component: () => import('../views/build/BuildTaskEdit.vue'),
meta: { title: '编辑构建任务', permission: '/build/tasks' }
},
+ {
+ path: 'tasks/copy',
+ name: 'build-task-copy',
+ component: () => import('../views/build/BuildTaskEdit.vue'),
+ meta: { title: '复制构建任务', permission: '/build/tasks' }
+ },
{
path: 'history',
name: 'build-history',
diff --git a/web/src/views/build/BuildTaskEdit.vue b/web/src/views/build/BuildTaskEdit.vue
index 7b97fde..ec06fdc 100644
--- a/web/src/views/build/BuildTaskEdit.vue
+++ b/web/src/views/build/BuildTaskEdit.vue
@@ -2,7 +2,7 @@
@@ -427,6 +427,7 @@ const router = useRouter();
const route = useRoute();
const formRef = ref();
const isEdit = ref(false);
+const isCopy = ref(false);
const submitLoading = ref(false);
const loading = ref(false);
const projectOptions = ref([]);
@@ -665,7 +666,7 @@ const handleSubmit = async () => {
submitData.external_script_token_id = undefined;
}
- if (!isEdit.value) {
+ if (!isEdit.value || isCopy.value) {
delete submitData.task_id;
}
@@ -674,14 +675,26 @@ const handleSubmit = async () => {
});
if (response.data.code === 200) {
- message.success(`${isEdit.value ? '更新' : '创建'}构建任务成功`);
+ let successMsg = '创建构建任务成功';
+ if (isEdit.value) {
+ successMsg = '更新构建任务成功';
+ } else if (isCopy.value) {
+ successMsg = '复制构建任务成功';
+ }
+ message.success(successMsg);
router.push('/build/tasks');
} else {
throw new Error(response.data.message);
}
} catch (error) {
console.error('Submit task error:', error);
- message.error(error.message || `${isEdit.value ? '更新' : '创建'}构建任务失败`);
+ let errorMsg = '创建构建任务失败';
+ if (isEdit.value) {
+ errorMsg = '更新构建任务失败';
+ } else if (isCopy.value) {
+ errorMsg = '复制构建任务失败';
+ }
+ message.error(error.message || errorMsg);
} finally {
submitLoading.value = false;
}
@@ -770,6 +783,90 @@ const loadTaskDetail = async (taskId) => {
}
};
+// 复制任务加载任务详情
+const loadTaskDetailForCopy = async (sourceTaskId) => {
+ try {
+ loading.value = true;
+ const token = localStorage.getItem('token');
+ const response = await axios.get(`/api/build/tasks/${sourceTaskId}`, {
+ headers: { 'Authorization': token }
+ });
+
+ if (response.data.code === 200) {
+ // 新任务不设置task_id
+ formState.task_id = '';
+ formState.name = response.data.data.name + ' - Copy';
+ formState.description = response.data.data.description;
+ formState.branch = response.data.data.branch;
+
+ // 外部脚本库配置
+ formState.use_external_script = response.data.data.use_external_script || false;
+ formState.external_script_repo_url = response.data.data.external_script_repo_url || '';
+ formState.external_script_directory = response.data.data.external_script_directory || '';
+ formState.external_script_branch = response.data.data.external_script_branch || '';
+ formState.external_script_token_id = response.data.data.external_script_token_id || undefined;
+
+ const stages = response.data.data.stages || [];
+ formState.stages = stages.map(stage => ({
+ name: stage.name || '',
+ script: stage.script || '',
+ }));
+
+ if (formState.stages.length === 0) {
+ formState.stages.push({
+ name: '构建',
+ script: '',
+ });
+ }
+
+ // 加载参数配置
+ const parameters = response.data.data.parameters || [];
+ formState.parameters = parameters.map(param => {
+ const choicesText = (param.choices || []).join('\n');
+ const defaultValuesText = (param.default_values || []).join(',');
+ return {
+ name: param.name || '',
+ description: param.description || '',
+ choices: param.choices || [],
+ choicesText: choicesText,
+ choiceOptions: (param.choices || []).map(choice => ({
+ label: choice,
+ value: choice
+ })),
+ default_values: param.default_values || [],
+ defaultValuesText: defaultValuesText,
+ };
+ });
+
+ formState.notification_channels = response.data.data.notification_channels || [];
+
+ if (response.data.data.project) {
+ formState.project_id = response.data.data.project.project_id;
+ }
+ if (response.data.data.environment) {
+ formState.environment_id = response.data.data.environment.environment_id;
+ }
+ if (response.data.data.git_token) {
+ formState.git_token_id = response.data.data.git_token.credential_id;
+ }
+
+ // 加载相关选项数据
+ await Promise.all([
+ loadProjects(),
+ loadEnvironments(),
+ loadGitCredentials()
+ ]);
+ } else {
+ message.error(response.data.message || '加载任务详情失败');
+ }
+ } catch (error) {
+ console.error('加载任务详情失败:', error);
+ message.error('加载任务详情失败');
+ } finally {
+ loading.value = false;
+ }
+};
+
// 过滤选项方法
const filterOption = (input, option) => {
return (
@@ -861,9 +958,14 @@ const truncateUrl = (url) => {
onMounted(async () => {
const taskId = route.query.task_id;
+ const sourceTaskId = route.query.source_task_id;
+
if (taskId) {
isEdit.value = true;
await loadTaskDetail(taskId);
+ } else if (sourceTaskId) {
+ isCopy.value = true;
+ await loadTaskDetailForCopy(sourceTaskId);
} else {
loadProjects();
loadEnvironments();
diff --git a/web/src/views/build/BuildTasks.vue b/web/src/views/build/BuildTasks.vue
index 5e3d8b2..d22456b 100644
--- a/web/src/views/build/BuildTasks.vue
+++ b/web/src/views/build/BuildTasks.vue
@@ -194,6 +194,9 @@
编辑
+
+ 复制
+
{
});
};
+// 处理复制任务
+const handleCopy = (record) => {
+ // 检查权限
+ const module = 'build_task';
+ const action = 'create';
+ if (!hasFunctionPermission(module, action)) {
+ message.error('你没有创建构建任务的权限');
+ return;
+ }
+
+ stopLogUpdate(); // 确保在跳转前清理定时器
+ router.push({
+ name: 'build-task-copy',
+ query: { source_task_id: record.task_id }
+ });
+};
+
// 下载日志
const handleDownloadLog = async () => {
if (!selectedHistoryId.value) {
@@ -1410,6 +1431,8 @@ const handleViewBuildDetail = (record) => {
}
});
};
+
+