long-git-cli 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/devops/ui/server.d.ts.map +1 -1
- package/dist/devops/ui/server.js +6 -1
- package/dist/devops/ui/server.js.map +1 -1
- package/package.json +2 -1
- package/src/devops/ui/public/css/components.css +397 -0
- package/src/devops/ui/public/css/main.css +381 -0
- package/src/devops/ui/public/index.html +489 -0
- package/src/devops/ui/public/js/api.js +116 -0
- package/src/devops/ui/public/js/app.js +104 -0
- package/src/devops/ui/public/js/components.js +267 -0
- package/src/devops/ui/public/js/config-page.js +339 -0
- package/src/devops/ui/public/js/deploy-page.js +158 -0
- package/src/devops/ui/public/js/full-deploy-page.js +247 -0
- package/src/devops/ui/public/js/pipeline-page.js +364 -0
- package/src/devops/ui/public/js/projects-page.js +659 -0
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目管理页面逻辑
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const ProjectsPage = {
|
|
6
|
+
projects: {},
|
|
7
|
+
currentEditingProject: null,
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 初始化项目管理页面
|
|
11
|
+
*/
|
|
12
|
+
async init() {
|
|
13
|
+
/** 加载项目列表 */
|
|
14
|
+
await this.loadProjects();
|
|
15
|
+
|
|
16
|
+
/** 绑定事件 */
|
|
17
|
+
this.bindEvents();
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 加载项目列表
|
|
22
|
+
*/
|
|
23
|
+
async loadProjects() {
|
|
24
|
+
try {
|
|
25
|
+
const data = await API.get("/projects");
|
|
26
|
+
this.projects = data.projects || {};
|
|
27
|
+
this.renderProjects();
|
|
28
|
+
} catch (error) {
|
|
29
|
+
Components.Toast.error("加载项目列表失败: " + error.message);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 绑定事件
|
|
35
|
+
*/
|
|
36
|
+
bindEvents() {
|
|
37
|
+
/** 添加项目按钮 */
|
|
38
|
+
document.getElementById("add-project-btn").addEventListener("click", () => {
|
|
39
|
+
this.showProjectModal();
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 渲染项目列表
|
|
45
|
+
*/
|
|
46
|
+
renderProjects() {
|
|
47
|
+
const container = document.getElementById("projects-list");
|
|
48
|
+
const emptyState = document.getElementById("projects-empty");
|
|
49
|
+
const projectPaths = Object.keys(this.projects);
|
|
50
|
+
|
|
51
|
+
if (projectPaths.length === 0) {
|
|
52
|
+
container.innerHTML = "";
|
|
53
|
+
emptyState.classList.remove("hidden");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
emptyState.classList.add("hidden");
|
|
58
|
+
container.innerHTML = "";
|
|
59
|
+
|
|
60
|
+
projectPaths.forEach((projectPath) => {
|
|
61
|
+
const project = this.projects[projectPath];
|
|
62
|
+
const projectCard = this.createProjectCard(project);
|
|
63
|
+
container.insertAdjacentHTML("beforeend", projectCard);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** 绑定卡片事件 */
|
|
67
|
+
projectPaths.forEach((projectPath) => {
|
|
68
|
+
const editBtn = document.querySelector(
|
|
69
|
+
`[data-project-path="${projectPath}"] .edit-project-btn`
|
|
70
|
+
);
|
|
71
|
+
const deleteBtn = document.querySelector(
|
|
72
|
+
`[data-project-path="${projectPath}"] .delete-project-btn`
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (editBtn) {
|
|
76
|
+
editBtn.addEventListener("click", () => {
|
|
77
|
+
this.showProjectModal(this.projects[projectPath]);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (deleteBtn) {
|
|
82
|
+
deleteBtn.addEventListener("click", () => {
|
|
83
|
+
this.deleteProject(projectPath);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 创建项目卡片 HTML
|
|
91
|
+
*/
|
|
92
|
+
createProjectCard(project) {
|
|
93
|
+
const envs = Object.keys(project.environments || {});
|
|
94
|
+
|
|
95
|
+
return `
|
|
96
|
+
<div class="card mb-lg" data-project-path="${project.path}">
|
|
97
|
+
<div class="card-header">
|
|
98
|
+
<div class="flex justify-between items-center">
|
|
99
|
+
<div>
|
|
100
|
+
<h3 class="card-title" style="margin-bottom: 0.25rem;">📁 ${project.name}</h3>
|
|
101
|
+
<p style="color: var(--text-secondary); font-size: 0.875rem; margin: 0;">
|
|
102
|
+
${project.path}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="flex gap-sm">
|
|
106
|
+
<button type="button" class="btn btn-secondary btn-sm edit-project-btn">
|
|
107
|
+
编辑
|
|
108
|
+
</button>
|
|
109
|
+
<button type="button" class="btn btn-danger btn-sm delete-project-btn">
|
|
110
|
+
删除
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="card-body">
|
|
116
|
+
<div class="mb-md">
|
|
117
|
+
<strong style="color: var(--text-primary);">Bitbucket 仓库:</strong>
|
|
118
|
+
<span style="color: var(--text-secondary);">
|
|
119
|
+
${project.repository.workspace} / ${project.repository.repoSlug}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="mb-md">
|
|
123
|
+
<strong style="color: var(--text-primary);">Tag 格式:</strong>
|
|
124
|
+
<span class="badge badge-primary">${project.tagFormat || "未设置"}</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div>
|
|
127
|
+
<strong style="color: var(--text-primary);">环境:</strong>
|
|
128
|
+
${envs.map((env) => `<span class="badge badge-success">${env}</span>`).join(" ")}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
`;
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 显示项目模态框
|
|
137
|
+
*/
|
|
138
|
+
showProjectModal(project = null) {
|
|
139
|
+
this.currentEditingProject = project;
|
|
140
|
+
const isEdit = !!project;
|
|
141
|
+
|
|
142
|
+
const modalContent = `
|
|
143
|
+
<form id="project-form">
|
|
144
|
+
<div class="form-group">
|
|
145
|
+
<label class="form-label">项目名称</label>
|
|
146
|
+
<input
|
|
147
|
+
type="text"
|
|
148
|
+
class="form-input"
|
|
149
|
+
id="project-name"
|
|
150
|
+
placeholder="My Project"
|
|
151
|
+
value="${project?.name || ""}"
|
|
152
|
+
required
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="form-group">
|
|
157
|
+
<label class="form-label">项目路径</label>
|
|
158
|
+
${
|
|
159
|
+
!isEdit
|
|
160
|
+
? `<button type="button" class="btn btn-primary" id="select-folder-btn" style="width: 100%;">
|
|
161
|
+
📁 选择项目文件夹
|
|
162
|
+
</button>`
|
|
163
|
+
: `<div style="padding: 0.75rem; background-color: var(--bg-secondary); border-radius: var(--radius-md); border: 1px solid var(--border-color);">
|
|
164
|
+
<div style="color: var(--text-primary); font-size: 0.875rem;">
|
|
165
|
+
📁 ${project?.path || ""}
|
|
166
|
+
</div>
|
|
167
|
+
</div>`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
<!-- 隐藏字段:存储路径和解析的 workspace、repoSlug -->
|
|
171
|
+
<input type="hidden" id="project-path" value="${project?.path || ""}" required />
|
|
172
|
+
<input type="hidden" id="project-workspace" value="${project?.repository?.workspace || ""}" />
|
|
173
|
+
<input type="hidden" id="project-repo-slug" value="${project?.repository?.repoSlug || ""}" />
|
|
174
|
+
|
|
175
|
+
<!-- 显示选择的路径和解析结果 -->
|
|
176
|
+
<div id="path-info-display" style="display: ${project?.path ? "block" : "none"}; margin-top: 0.75rem;">
|
|
177
|
+
<div style="padding: 0.75rem; background-color: var(--bg-secondary); border-radius: var(--radius-md); border: 1px solid var(--border-color);">
|
|
178
|
+
<div style="color: var(--text-primary); font-size: 0.875rem; margin-bottom: 0.5rem;">
|
|
179
|
+
<strong>📁 路径:</strong> <span id="display-path" style="color: var(--text-secondary);">${project?.path || ""}</span>
|
|
180
|
+
</div>
|
|
181
|
+
<div id="git-info-display" style="display: ${project?.repository?.workspace ? "block" : "none"}; padding-top: 0.5rem; border-top: 1px solid var(--border-color);">
|
|
182
|
+
<div style="color: var(--text-primary); font-size: 0.875rem;">
|
|
183
|
+
<strong>📦 Bitbucket:</strong>
|
|
184
|
+
<span id="display-workspace" style="color: var(--text-secondary);">${project?.repository?.workspace || ""}</span>
|
|
185
|
+
<span style="color: var(--text-secondary);"> / </span>
|
|
186
|
+
<span id="display-repo-slug" style="color: var(--text-secondary);">${project?.repository?.repoSlug || ""}</span>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="form-group">
|
|
194
|
+
<label class="form-label">Tag 格式</label>
|
|
195
|
+
<input
|
|
196
|
+
type="text"
|
|
197
|
+
class="form-input"
|
|
198
|
+
id="project-tag-format"
|
|
199
|
+
placeholder="v00.00.0000"
|
|
200
|
+
value="${project?.tagFormat || ""}"
|
|
201
|
+
required
|
|
202
|
+
/>
|
|
203
|
+
<small style="color: var(--text-secondary); font-size: 0.75rem;">
|
|
204
|
+
统一的 Tag 格式,如 test-v00.00.0000 或 uat-v00.00.0000
|
|
205
|
+
</small>
|
|
206
|
+
<div id="tag-format-preview" style="margin-top: 0.5rem; padding: 0.5rem; background-color: var(--bg-secondary); border-radius: var(--radius-sm); display: none;">
|
|
207
|
+
<small style="color: var(--text-secondary); font-size: 0.75rem;">
|
|
208
|
+
<strong>示例:</strong> <span id="tag-format-example"></span>
|
|
209
|
+
</small>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div class="form-group">
|
|
214
|
+
<label class="form-label">环境配置</label>
|
|
215
|
+
<div id="environments-list">
|
|
216
|
+
${this.createEnvironmentsListHtml(project?.environments || {})}
|
|
217
|
+
</div>
|
|
218
|
+
<button type="button" class="btn btn-secondary btn-sm mt-sm" id="add-env-btn">
|
|
219
|
+
+ 添加环境
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
</form>
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
Components.Modal.show({
|
|
226
|
+
title: isEdit ? "编辑项目" : "添加项目",
|
|
227
|
+
content: modalContent,
|
|
228
|
+
showCancel: true,
|
|
229
|
+
cancelText: "取消",
|
|
230
|
+
confirmText: "保存",
|
|
231
|
+
onConfirm: async () => {
|
|
232
|
+
// 收集数据并保存
|
|
233
|
+
const projectData = this.collectProjectData();
|
|
234
|
+
if (projectData) {
|
|
235
|
+
await this.saveProjectWithData(projectData);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/** 绑定添加环境按钮和选择文件夹按钮 */
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
document.getElementById("add-env-btn")?.addEventListener("click", () => {
|
|
243
|
+
this.addEnvironment();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
/** 绑定选择文件夹按钮(仅在新增项目时) */
|
|
247
|
+
if (!isEdit) {
|
|
248
|
+
document.getElementById("select-folder-btn")?.addEventListener("click", () => {
|
|
249
|
+
this.selectProjectFolder();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 绑定 Tag 格式输入框,实时预览 */
|
|
254
|
+
document.getElementById("project-tag-format")?.addEventListener("input", (e) => {
|
|
255
|
+
this.previewTagFormat(e.target.value);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
/** 如果是编辑模式,显示当前格式的预览 */
|
|
259
|
+
if (project?.tagFormat) {
|
|
260
|
+
this.previewTagFormat(project.tagFormat);
|
|
261
|
+
}
|
|
262
|
+
}, 100);
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 预览 Tag 格式
|
|
267
|
+
*/
|
|
268
|
+
previewTagFormat(format) {
|
|
269
|
+
const previewDiv = document.getElementById("tag-format-preview");
|
|
270
|
+
const exampleSpan = document.getElementById("tag-format-example");
|
|
271
|
+
|
|
272
|
+
if (!format || !format.trim()) {
|
|
273
|
+
previewDiv.style.display = "none";
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
/** 生成示例 tag */
|
|
279
|
+
const example = this.generateTagExample(format);
|
|
280
|
+
exampleSpan.textContent = example;
|
|
281
|
+
previewDiv.style.display = "block";
|
|
282
|
+
} catch (error) {
|
|
283
|
+
exampleSpan.textContent = "格式无效";
|
|
284
|
+
previewDiv.style.display = "block";
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 生成 Tag 示例
|
|
290
|
+
*/
|
|
291
|
+
generateTagExample(format) {
|
|
292
|
+
/** 简单的示例生成:将 0 替换为示例数字 */
|
|
293
|
+
return format.replace(/0+/g, (match) => {
|
|
294
|
+
if (match.length === 2) return "01";
|
|
295
|
+
if (match.length === 4) return "0001";
|
|
296
|
+
return match;
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 创建环境列表 HTML
|
|
302
|
+
*/
|
|
303
|
+
createEnvironmentsListHtml(environments) {
|
|
304
|
+
const envs = Object.keys(environments);
|
|
305
|
+
if (envs.length === 0) {
|
|
306
|
+
return '<p style="color: var(--text-secondary); font-size: 0.875rem;">暂无环境,点击下方按钮添加</p>';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return envs
|
|
310
|
+
.map((env) => {
|
|
311
|
+
const config = environments[env];
|
|
312
|
+
return `
|
|
313
|
+
<div class="env-item mb-md" style="padding: 1rem; border: 1px solid var(--border-color); border-radius: var(--radius-md); background-color: var(--bg-color);">
|
|
314
|
+
<div class="flex justify-between items-center mb-sm">
|
|
315
|
+
<strong style="font-size: 0.875rem;">${env}</strong>
|
|
316
|
+
<button type="button" class="btn btn-danger btn-sm" onclick="ProjectsPage.removeEnvironment('${env}')">
|
|
317
|
+
删除
|
|
318
|
+
</button>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="form-group" style="margin-bottom: 0;">
|
|
321
|
+
<label class="form-label" style="font-size: 0.75rem;">Jenkins Job 名称</label>
|
|
322
|
+
<input
|
|
323
|
+
type="text"
|
|
324
|
+
class="form-input env-job-name"
|
|
325
|
+
data-env="${env}"
|
|
326
|
+
placeholder="deploy-${env}"
|
|
327
|
+
value="${config.jenkinsJobName || ""}"
|
|
328
|
+
style="font-size: 0.875rem;"
|
|
329
|
+
required
|
|
330
|
+
/>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
`;
|
|
334
|
+
})
|
|
335
|
+
.join("");
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 选择项目文件夹并自动解析 Git 信息
|
|
340
|
+
*/
|
|
341
|
+
async selectProjectFolder() {
|
|
342
|
+
try {
|
|
343
|
+
Components.Loading.show("正在打开文件夹选择器...");
|
|
344
|
+
|
|
345
|
+
/** 调用后端 API 打开系统文件夹选择对话框 */
|
|
346
|
+
const response = await API.post("/select-folder", {});
|
|
347
|
+
|
|
348
|
+
Components.Loading.hide();
|
|
349
|
+
|
|
350
|
+
if (!response.path) {
|
|
351
|
+
Components.Toast.error("未选择文件夹");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** 设置路径 */
|
|
356
|
+
document.getElementById("project-path").value = response.path;
|
|
357
|
+
|
|
358
|
+
/** 自动解析 Git 信息 */
|
|
359
|
+
await this.parseGitInfo(response.path);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
Components.Loading.hide();
|
|
362
|
+
|
|
363
|
+
if (error.message.includes("用户取消")) {
|
|
364
|
+
/** 用户取消选择,不显示错误 */
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
Components.Toast.error("选择文件夹失败: " + error.message);
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 解析 Git 仓库信息
|
|
374
|
+
*/
|
|
375
|
+
async parseGitInfo(projectPath) {
|
|
376
|
+
try {
|
|
377
|
+
if (!projectPath) {
|
|
378
|
+
projectPath = document.getElementById("project-path").value.trim();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!projectPath) {
|
|
382
|
+
Components.Toast.error("请先选择项目路径");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
Components.Loading.show("正在解析 Git 信息...");
|
|
387
|
+
|
|
388
|
+
const response = await API.post("/parse-git-repo", { projectPath });
|
|
389
|
+
|
|
390
|
+
Components.Loading.hide();
|
|
391
|
+
|
|
392
|
+
/** 显示路径信息 */
|
|
393
|
+
document.getElementById("display-path").textContent = projectPath;
|
|
394
|
+
document.getElementById("path-info-display").style.display = "block";
|
|
395
|
+
|
|
396
|
+
if (response.warning) {
|
|
397
|
+
/** 不是 Bitbucket 仓库,显示警告 */
|
|
398
|
+
Components.Toast.error(response.warning);
|
|
399
|
+
Components.Modal.confirm(
|
|
400
|
+
`检测到 Git 仓库,但不是 Bitbucket 仓库。\n\nRemote URL: ${response.remoteUrl}\n\n是否手动填写 Bitbucket 信息?`,
|
|
401
|
+
() => {
|
|
402
|
+
/** 显示输入框让用户手动填写 */
|
|
403
|
+
const workspace = prompt("请输入 Bitbucket Workspace:");
|
|
404
|
+
const repoSlug = prompt("请输入 Bitbucket Repo Slug:");
|
|
405
|
+
|
|
406
|
+
if (workspace && repoSlug) {
|
|
407
|
+
document.getElementById("project-workspace").value = workspace.trim();
|
|
408
|
+
document.getElementById("project-repo-slug").value = repoSlug.trim();
|
|
409
|
+
document.getElementById("display-workspace").textContent = workspace.trim();
|
|
410
|
+
document.getElementById("display-repo-slug").textContent = repoSlug.trim();
|
|
411
|
+
document.getElementById("git-info-display").style.display = "block";
|
|
412
|
+
Components.Toast.success("已手动设置 Bitbucket 信息");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
} else {
|
|
417
|
+
/** 成功解析,自动填充 */
|
|
418
|
+
document.getElementById("project-workspace").value = response.workspace;
|
|
419
|
+
document.getElementById("project-repo-slug").value = response.repoSlug;
|
|
420
|
+
document.getElementById("display-workspace").textContent = response.workspace;
|
|
421
|
+
document.getElementById("display-repo-slug").textContent = response.repoSlug;
|
|
422
|
+
document.getElementById("git-info-display").style.display = "block";
|
|
423
|
+
Components.Toast.success("Git 信息解析成功");
|
|
424
|
+
}
|
|
425
|
+
} catch (error) {
|
|
426
|
+
Components.Loading.hide();
|
|
427
|
+
|
|
428
|
+
/** 显示路径(即使解析失败) */
|
|
429
|
+
const path = document.getElementById("project-path").value;
|
|
430
|
+
if (path) {
|
|
431
|
+
document.getElementById("display-path").textContent = path;
|
|
432
|
+
document.getElementById("path-info-display").style.display = "block";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
Components.Toast.error("解析失败: " + error.message);
|
|
436
|
+
|
|
437
|
+
/** 如果是 Git 仓库相关错误,提示用户 */
|
|
438
|
+
if (error.message.includes("不是 Git 仓库")) {
|
|
439
|
+
Components.Modal.confirm(
|
|
440
|
+
"该目录不是 Git 仓库,是否需要初始化 Git?\n\n请在终端中执行:\ncd " +
|
|
441
|
+
document.getElementById("project-path").value.trim() +
|
|
442
|
+
"\ngit init\ngit remote add origin <your-bitbucket-repo-url>",
|
|
443
|
+
() => {
|
|
444
|
+
Components.Toast.error("请先初始化 Git 仓库后重试");
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 添加环境
|
|
453
|
+
*/
|
|
454
|
+
addEnvironment() {
|
|
455
|
+
const envName = prompt("请输入环境名称(如 test-01, uat 等):");
|
|
456
|
+
if (!envName || !envName.trim()) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const trimmedName = envName.trim();
|
|
461
|
+
const container = document.getElementById("environments-list");
|
|
462
|
+
|
|
463
|
+
/** 检查是否已存在 */
|
|
464
|
+
const existing = container.querySelector(`[data-env="${trimmedName}"]`);
|
|
465
|
+
if (existing) {
|
|
466
|
+
Components.Toast.error("该环境已存在");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const envHtml = `
|
|
471
|
+
<div class="env-item mb-md" style="padding: 1rem; border: 1px solid var(--border-color); border-radius: var(--radius-md); background-color: var(--bg-color);">
|
|
472
|
+
<div class="flex justify-between items-center mb-sm">
|
|
473
|
+
<strong style="font-size: 0.875rem;">${trimmedName}</strong>
|
|
474
|
+
<button type="button" class="btn btn-danger btn-sm" onclick="ProjectsPage.removeEnvironment('${trimmedName}')">
|
|
475
|
+
删除
|
|
476
|
+
</button>
|
|
477
|
+
</div>
|
|
478
|
+
<div class="form-group" style="margin-bottom: 0;">
|
|
479
|
+
<label class="form-label" style="font-size: 0.75rem;">Jenkins Job 名称</label>
|
|
480
|
+
<input
|
|
481
|
+
type="text"
|
|
482
|
+
class="form-input env-job-name"
|
|
483
|
+
data-env="${trimmedName}"
|
|
484
|
+
placeholder="deploy-${trimmedName}"
|
|
485
|
+
value=""
|
|
486
|
+
style="font-size: 0.875rem;"
|
|
487
|
+
required
|
|
488
|
+
/>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
`;
|
|
492
|
+
|
|
493
|
+
/** 移除空状态提示 */
|
|
494
|
+
const emptyMsg = container.querySelector("p");
|
|
495
|
+
if (emptyMsg) {
|
|
496
|
+
emptyMsg.remove();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
container.insertAdjacentHTML("beforeend", envHtml);
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 移除环境
|
|
504
|
+
*/
|
|
505
|
+
removeEnvironment(envName) {
|
|
506
|
+
const container = document.getElementById("environments-list");
|
|
507
|
+
const envItem = container.querySelector(`[data-env="${envName}"]`)?.closest(".env-item");
|
|
508
|
+
if (envItem) {
|
|
509
|
+
envItem.remove();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** 如果没有环境了,显示空状态 */
|
|
513
|
+
if (container.querySelectorAll(".env-item").length === 0) {
|
|
514
|
+
container.innerHTML = '<p style="color: var(--text-secondary); font-size: 0.875rem;">暂无环境,点击下方按钮添加</p>';
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* 收集项目数据(在模态框关闭前调用)
|
|
520
|
+
*/
|
|
521
|
+
collectProjectData() {
|
|
522
|
+
try {
|
|
523
|
+
const name = document.getElementById("project-name")?.value.trim();
|
|
524
|
+
const path = document.getElementById("project-path")?.value.trim();
|
|
525
|
+
const workspace = document.getElementById("project-workspace")?.value.trim();
|
|
526
|
+
const repoSlug = document.getElementById("project-repo-slug")?.value.trim();
|
|
527
|
+
const tagFormat = document.getElementById("project-tag-format")?.value.trim();
|
|
528
|
+
|
|
529
|
+
console.log('📝 收集项目数据:');
|
|
530
|
+
console.log(' name:', name);
|
|
531
|
+
console.log(' path:', path);
|
|
532
|
+
console.log(' workspace:', workspace);
|
|
533
|
+
console.log(' repoSlug:', repoSlug);
|
|
534
|
+
console.log(' tagFormat:', tagFormat);
|
|
535
|
+
|
|
536
|
+
// 详细检查每个字段
|
|
537
|
+
const missingFields = [];
|
|
538
|
+
if (!name) missingFields.push('项目名称');
|
|
539
|
+
if (!path) missingFields.push('项目路径');
|
|
540
|
+
if (!workspace) missingFields.push('Workspace');
|
|
541
|
+
if (!repoSlug) missingFields.push('Repo Slug');
|
|
542
|
+
if (!tagFormat) missingFields.push('Tag 格式');
|
|
543
|
+
|
|
544
|
+
if (missingFields.length > 0) {
|
|
545
|
+
console.error('❌ 验证失败:缺少必填字段:', missingFields.join(', '));
|
|
546
|
+
Components.Toast.error(`请填写所有必填字段:${missingFields.join('、')}`);
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** 收集环境配置 */
|
|
551
|
+
const environments = {};
|
|
552
|
+
const envInputs = document.querySelectorAll(".env-job-name");
|
|
553
|
+
|
|
554
|
+
console.log('📝 收集环境配置,找到', envInputs.length, '个环境输入框');
|
|
555
|
+
|
|
556
|
+
envInputs.forEach((input) => {
|
|
557
|
+
const env = input.getAttribute("data-env");
|
|
558
|
+
const jobName = input.value.trim();
|
|
559
|
+
|
|
560
|
+
console.log(' 环境:', env, '-> Job:', jobName);
|
|
561
|
+
|
|
562
|
+
if (env && jobName) {
|
|
563
|
+
environments[env] = {
|
|
564
|
+
name: env,
|
|
565
|
+
tagFormat: tagFormat, // 使用统一的 tag 格式
|
|
566
|
+
jenkinsJobName: jobName,
|
|
567
|
+
jenkinsTagParameter: "TAG_VERSION", // 固定参数名
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
console.log('📝 收集到的环境配置:', environments);
|
|
573
|
+
|
|
574
|
+
if (Object.keys(environments).length === 0) {
|
|
575
|
+
console.error('❌ 验证失败:没有环境配置');
|
|
576
|
+
Components.Toast.error("请至少添加一个环境");
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const projectConfig = {
|
|
581
|
+
name,
|
|
582
|
+
path,
|
|
583
|
+
repository: {
|
|
584
|
+
workspace,
|
|
585
|
+
repoSlug,
|
|
586
|
+
},
|
|
587
|
+
jenkinsInstance: "app", // 固定为 app
|
|
588
|
+
tagFormat, // 项目级别的 tag 格式
|
|
589
|
+
environments,
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
console.log('📝 完整项目配置:', JSON.stringify(projectConfig, null, 2));
|
|
593
|
+
|
|
594
|
+
return projectConfig;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error('❌ 收集数据失败:', error);
|
|
597
|
+
Components.Toast.error("收集数据失败: " + error.message);
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 使用收集的数据保存项目
|
|
604
|
+
*/
|
|
605
|
+
async saveProjectWithData(projectConfig) {
|
|
606
|
+
try {
|
|
607
|
+
Components.Loading.show("保存项目中...");
|
|
608
|
+
|
|
609
|
+
if (this.currentEditingProject) {
|
|
610
|
+
/** 更新项目 */
|
|
611
|
+
console.log('📝 发送更新请求:', `/projects/${encodeURIComponent(projectConfig.path)}`);
|
|
612
|
+
await API.put(`/projects/${encodeURIComponent(projectConfig.path)}`, projectConfig);
|
|
613
|
+
} else {
|
|
614
|
+
/** 添加项目 */
|
|
615
|
+
console.log('📝 发送添加请求: /projects');
|
|
616
|
+
await API.post("/projects", projectConfig);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
console.log('✅ 保存成功');
|
|
620
|
+
Components.Loading.hide();
|
|
621
|
+
Components.Toast.success("项目保存成功");
|
|
622
|
+
|
|
623
|
+
/** 重新加载项目列表 */
|
|
624
|
+
await this.loadProjects();
|
|
625
|
+
} catch (error) {
|
|
626
|
+
console.error('❌ 保存失败:', error);
|
|
627
|
+
Components.Loading.hide();
|
|
628
|
+
Components.Toast.error("保存项目失败: " + error.message);
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* 保存项目(已废弃,保留用于兼容)
|
|
634
|
+
*/
|
|
635
|
+
async saveProject() {
|
|
636
|
+
const projectData = this.collectProjectData();
|
|
637
|
+
if (projectData) {
|
|
638
|
+
await this.saveProjectWithData(projectData);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* 删除项目
|
|
644
|
+
*/
|
|
645
|
+
async deleteProject(projectPath) {
|
|
646
|
+
Components.Modal.confirm(`确定要删除项目 "${this.projects[projectPath].name}" 吗?`, async () => {
|
|
647
|
+
try {
|
|
648
|
+
Components.Loading.show("删除项目中...");
|
|
649
|
+
await API.delete(`/projects/${encodeURIComponent(projectPath)}`);
|
|
650
|
+
Components.Loading.hide();
|
|
651
|
+
Components.Toast.success("项目删除成功");
|
|
652
|
+
await this.loadProjects();
|
|
653
|
+
} catch (error) {
|
|
654
|
+
Components.Loading.hide();
|
|
655
|
+
Components.Toast.error("删除项目失败: " + error.message);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
},
|
|
659
|
+
};
|