From 9ecb392a41f438314a4f16dc9c479b8a0b1c675d Mon Sep 17 00:00:00 2001 From: loren Date: Fri, 30 Jan 2026 10:58:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=AD=A3=E5=BC=8F=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=9B=BE=E5=BD=A2=E5=8C=96=E6=98=A0=E5=B0=84=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=8D=8A=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/editor.html | 17 + public/mappingsEditor.js | 1124 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 1141 insertions(+) create mode 100644 public/mappingsEditor.js diff --git a/public/editor.html b/public/editor.html index 10f4726..5027233 100644 --- a/public/editor.html +++ b/public/editor.html @@ -257,6 +257,23 @@ (Transitions) + + +
+

输入工单 Key 扫描该工单当前可用的状态流转,扫描结果会追加到下拉菜单中

+
+ + +
+
+ +
+
关闭 (Close) diff --git a/public/mappingsEditor.js b/public/mappingsEditor.js new file mode 100644 index 0000000..8cd298c --- /dev/null +++ b/public/mappingsEditor.js @@ -0,0 +1,1124 @@ +let fetchedData = null; +let extractedSprintId = null; +let extractedSprintName = null; +let sprintMappings = []; +let currentRepoName = null; +let existingMappings = null; + +//默认的Gitea标签建议 +const defaultLabels = { + 'High': 'high', 'Highest': 'highest', 'Medium': 'medium', 'Low': 'low', 'Lowest': 'lowest', + 'Bug': 'bug', 'Story': 'story', 'Task': 'task', 'New Feature': 'feature' +}; + +//为Jira字段添加新的Gitea标签输入框 +function addLabel(type, jiraId, jiraName) { + const containerClass = type === 'priority' ? 'priority-labels-container' : 'type-labels-container'; + const dataType = type === 'priority' ? 'priority' : 'issuetype'; + const removeFunc = type === 'priority' ? 'removePriorityLabelInput' : 'removeTypeLabelInput'; + const placeholder = type === 'priority' ? 'urgent' : 'defect'; + + const container = document.querySelector(`.${containerClass}[data-jira-id="${jiraId}"]`); + if (!container) return; + + const newInput = document.createElement('div'); + newInput.className = 'flex items-center gap-2'; + newInput.innerHTML = ` + Gitea标签: + + + `; + container.appendChild(newInput); + + //为新输入框绑定实时更新事件 + const input = newInput.querySelector('input'); + if (input) { + input.addEventListener('input', updatePreview); + } + + updateLabelDeleteButtons(type, jiraId); + updatePreview(); +} + +//删除标签输入框 +function removeLabelInput(type, btn) { + const containerClass = type === 'priority' ? 'priority-labels-container' : 'type-labels-container'; + const container = btn.closest(`.${containerClass}`); + const jiraId = container.dataset.jiraId; + btn.closest('.flex').remove(); + updateLabelDeleteButtons(type, jiraId); + updatePreview(); +} + +//更新标签删除按钮的可见性 +function updateLabelDeleteButtons(type, jiraId) { + const containerClass = type === 'priority' ? 'priority-labels-container' : 'type-labels-container'; + const removeFunc = type === 'priority' ? 'removePriorityLabelInput' : 'removeTypeLabelInput'; + + const container = document.querySelector(`.${containerClass}[data-jira-id="${jiraId}"]`); + if (!container) return; + + const inputs = container.querySelectorAll('.flex'); + inputs.forEach(input => { + const deleteBtn = input.querySelector(`button[onclick*="${removeFunc}"]`); + if (deleteBtn) { + if (inputs.length > 1) { + deleteBtn.classList.remove('opacity-0', 'pointer-events-none'); + } else { + deleteBtn.classList.add('opacity-0', 'pointer-events-none'); + } + } + }); +} + +//包装函数:保持HTML兼容性 +function addPriorityLabel(jiraId, jiraName) { addLabel('priority', jiraId, jiraName); } +function addTypeLabel(jiraId, jiraName) { addLabel('type', jiraId, jiraName); } +function removePriorityLabelInput(btn) { removeLabelInput('priority', btn); } +function removeTypeLabelInput(btn) { removeLabelInput('type', btn); } +function updatePriorityLabelDeleteButtons(jiraId) { updateLabelDeleteButtons('priority', jiraId); } +function updateTypeLabelDeleteButtons(jiraId) { updateLabelDeleteButtons('type', jiraId); } + +//页面加载时恢复缓存的配置 +window.addEventListener('DOMContentLoaded', () => { + loadGlobalSettings(); +}); + +//加载全局设置 +function loadGlobalSettings() { + const savedConfig = localStorage.getItem('jiraGlobalConfig'); + if (savedConfig) { + try { + const config = JSON.parse(savedConfig); + if (config.jiraUrl) document.getElementById('settingsJiraUrl').value = config.jiraUrl; + if (config.jiraUser) document.getElementById('settingsJiraUser').value = config.jiraUser; + if (config.jiraToken) document.getElementById('settingsJiraToken').value = config.jiraToken; + } catch (e) { + console.error('Failed to load saved config:', e); + } + } +} + +//打开设置窗口 +function openSettings() { + loadGlobalSettings(); + document.getElementById('settingsModal').classList.remove('hidden'); +} + +//关闭设置窗口 +function closeSettings() { + document.getElementById('settingsModal').classList.add('hidden'); +} + +//保存全局设置 +function saveSettings() { + const config = { + jiraUrl: document.getElementById('settingsJiraUrl').value.trim(), + jiraUser: document.getElementById('settingsJiraUser').value.trim(), + jiraToken: document.getElementById('settingsJiraToken').value.trim() + }; + + if (!config.jiraUrl) { + alert('请填写Jira地址'); + return; + } + + localStorage.setItem('jiraGlobalConfig', JSON.stringify(config)); + closeSettings(); + alert('全局设置已保存'); +} + +//获取全局设置 +function getGlobalSettings() { + const savedConfig = localStorage.getItem('jiraGlobalConfig'); + if (savedConfig) { + try { + return JSON.parse(savedConfig); + } catch (e) { + return {}; + } + } + return {}; +} + +//加载现有映射配置 +async function loadExistingMappings() { + const btn = document.getElementById('loadBtn'); + const errBox = document.getElementById('step0Error'); + + btn.disabled = true; + btn.innerText = '加载中...'; + errBox.classList.add('hidden'); + + try { + const res = await fetch('/editor/api/mappings'); + const data = await res.json(); + + if (!data.success) { + throw new Error(data.error); + } + + existingMappings = data.data; + const repos = Object.keys(existingMappings.repositories || {}); + + if (repos.length === 0) { + throw new Error('没有找到现有配置,请新建仓库配置'); + } + + //显示仓库列表 + const container = document.getElementById('repoListContainer'); + const list = document.getElementById('repoList'); + + list.innerHTML = repos.map(repo => ` +
+
+ ${repo} + (点击编辑) +
+
+ + + +
+
+ `).join(''); + + container.classList.remove('hidden'); + + } catch (e) { + showStep0Error(e.message); + } finally { + btn.disabled = false; + btn.innerText = '加载现有配置'; + } +} + +//选择要编辑的仓库 +async function selectRepo(repoName) { + currentRepoName = repoName; + const config = existingMappings.repositories[repoName]; + + //预填充基本字段 + document.getElementById('currentRepoName').innerText = repoName; + document.getElementById('projectKey').value = config.jira?.projectKey || ''; + + //预填充 Sprint 映射 + if (config.sprints) { + sprintMappings = Object.entries(config.sprints).map(([milestone, sprintId]) => ({ + milestone, + sprintId, + sprintName: '' + })); + renderSprintList(); + } + + //隐藏 Step 0,显示 Step 1 + document.getElementById('step0').classList.add('hidden'); + document.getElementById('step1').classList.remove('hidden'); + document.getElementById('step1').classList.add('step-active'); + + //如果有项目Key,自动扫描并预填充配置 + if (config.jira?.projectKey) { + const settings = getGlobalSettings(); + + if (settings.jiraUrl && (settings.jiraToken || settings.jiraUser)) { + await scanJiraAndLoadConfig(config); + } + } +} + +//新建仓库配置 +function createNewMapping() { + //清空之前的状态,避免残留数据 + sprintMappings = []; + fetchedData = null; + + document.getElementById('repoListContainer').classList.add('hidden'); + document.getElementById('newRepoContainer').classList.remove('hidden'); +} + +//确认新仓库名称 +function confirmNewRepo() { + const repoName = document.getElementById('newRepoName').value.trim(); + + if (!repoName) { + showStep0Error('请输入仓库名称'); + return; + } + + if (!/^[\w-]+\/[\w-]+$/.test(repoName)) { + showStep0Error('仓库名称格式不正确,应为: owner/repo'); + return; + } + + //清空之前的状态 + sprintMappings = []; + fetchedData = null; + + currentRepoName = repoName; + document.getElementById('currentRepoName').innerText = repoName; + + //隐藏Step 0,显示Step 1 + document.getElementById('step0').classList.add('hidden'); + document.getElementById('step1').classList.remove('hidden'); + document.getElementById('step1').classList.add('step-active'); +} + +//显示错误信息 +function showErrorMsg(elementId, msg) { + const el = document.getElementById(elementId); + el.innerText = msg; + el.classList.remove('hidden'); +} + +function showStep0Error(msg) { showErrorMsg('step0Error', msg); } + +function backToStart() { + //重置状态 + currentRepoName = null; + fetchedData = null; + sprintMappings = []; + + //清空UI显示 + document.getElementById('sprintListContainer').classList.add('hidden'); + document.getElementById('sprintList').innerHTML = ''; + + //隐藏所有步骤 + document.getElementById('step1').classList.add('hidden'); + document.getElementById('step2').classList.add('hidden'); + document.getElementById('step3').classList.add('hidden'); + + //显示 Step 0 + document.getElementById('step0').classList.remove('hidden'); + document.getElementById('step0').classList.add('step-active'); + + //重置输入框 + document.getElementById('repoListContainer').classList.add('hidden'); + document.getElementById('newRepoContainer').classList.add('hidden'); + document.getElementById('newRepoName').value = ''; +} + +//删除仓库 +async function deleteRepo(repoName) { + if (!confirm(`确定要删除仓库 "${repoName}" 的配置吗?\n\n此操作不可恢复!`)) { + return; + } + + try { + const res = await fetch('/editor/api/mappings/' + encodeURIComponent(repoName), { + method: 'DELETE' + }); + + const data = await res.json(); + + if (!data.success) { + throw new Error(data.error); + } + + alert('仓库配置已删除'); + + //重新加载仓库列表 + await loadExistingMappings(); + + } catch (e) { + alert('删除失败: ' + e.message); + } +} + +//打开改名模态窗口 +function openRenameModal(repoName) { + document.getElementById('renameOldName').value = repoName; + document.getElementById('renameNewName').value = repoName; + document.getElementById('renameError').classList.add('hidden'); + document.getElementById('renameModal').classList.remove('hidden'); + + //聚焦到新名称输入框并选中文本 + setTimeout(() => { + const input = document.getElementById('renameNewName'); + input.focus(); + input.select(); + }, 100); +} + +//关闭改名模态窗口 +function closeRenameModal() { + document.getElementById('renameModal').classList.add('hidden'); +} + +function showRenameError(msg) { showErrorMsg('renameError', msg); } + +//确认改名 +async function confirmRename() { + const oldName = document.getElementById('renameOldName').value.trim(); + const newName = document.getElementById('renameNewName').value.trim(); + const btn = document.getElementById('renameConfirmBtn'); + + //验证 + if (!newName) { + showRenameError('请输入新名称'); + return; + } + + if (newName === oldName) { + showRenameError('新名称与当前名称相同'); + return; + } + + if (!/^[\w-]+\/[\w-]+$/.test(newName)) { + showRenameError('仓库名称格式不正确,应为: owner/repo'); + return; + } + + //检查新名称是否已存在 + if (existingMappings && existingMappings.repositories && existingMappings.repositories[newName]) { + showRenameError('新名称已存在,请使用其他名称'); + return; + } + + btn.disabled = true; + btn.innerText = '改名中...'; + + try { + const res = await fetch('/editor/api/mappings/rename', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + oldName: oldName, + newName: newName + }) + }); + + const data = await res.json(); + + if (!data.success) { + throw new Error(data.error); + } + + alert('仓库已成功改名'); + closeRenameModal(); + + //重新加载仓库列表 + await loadExistingMappings(); + + } catch (e) { + showRenameError('改名失败: ' + e.message); + } finally { + btn.disabled = false; + btn.innerText = '确认改名'; + } +} + +//scanJiraAndLoadConfig 已合并到 scanJira(existingConfig) +async function scanJiraAndLoadConfig(existingConfig) { + return scanJira(existingConfig); +} + +//恢复标签配置到UI +function restoreLabelConfig(config, type) { + if (!config) return; + + const containerClass = type === 'priority' ? 'priority-labels-container' : 'type-labels-container'; + const dataType = type === 'priority' ? 'priority' : 'issuetype'; + const removeFunc = type === 'priority' ? 'removePriorityLabelInput' : 'removeTypeLabelInput'; + + //按JiraID 分组Gitea标签 + const byJiraId = {}; + Object.entries(config).forEach(([label, jiraId]) => { + if (!byJiraId[jiraId]) byJiraId[jiraId] = []; + byJiraId[jiraId].push(label); + }); + + //为每个Jira字段填充标签 + Object.entries(byJiraId).forEach(([jiraId, labels]) => { + const container = document.querySelector(`.${containerClass}[data-jira-id="${jiraId}"]`); + if (!container) return; + + container.innerHTML = ''; + labels.forEach(label => { + const inputDiv = document.createElement('div'); + inputDiv.className = 'flex items-center gap-2'; + inputDiv.innerHTML = ` + Gitea标签: + + + `; + container.appendChild(inputDiv); + }); + }); +} + +//将现有配置恢复到UI +function restoreConfigToUI(config) { + restoreLabelConfig(config.priorities, 'priority'); + restoreLabelConfig(config.types, 'type'); + + if (config.jira?.defaultType) { + document.getElementById('defaultTypeSelect').value = config.jira.defaultType; + } + + if (config.transitions) { + if (config.transitions.close) document.getElementById('transClose').value = config.transitions.close; + if (config.transitions.reopen) document.getElementById('transReopen').value = config.transitions.reopen; + if (config.transitions.in_progress) document.getElementById('transProgress').value = config.transitions.in_progress; + } + + updatePreview(); +} + +async function fetchSprintId() { + const btn = document.getElementById('fetchSprintBtn'); + const resultBox = document.getElementById('sprintResult'); + const errBox = document.getElementById('sprintError'); + const milestoneBox = document.getElementById('sprintMilestone'); + + const settings = getGlobalSettings(); + const baseUrl = settings.jiraUrl; + const username = settings.jiraUser; + const token = settings.jiraToken; + const issueKey = document.getElementById('issueKey').value.trim().toUpperCase(); + + if (!baseUrl) { + showSprintError("请先在全局设置中配置Jira地址"); + return; + } + + if (!issueKey) { + showSprintError("请填写工单 Key"); + return; + } + + btn.disabled = true; + btn.innerText = "提取中..."; + errBox.classList.add('hidden'); + resultBox.innerHTML = ''; + + try { + //构造认证头(支持 PAT 和 Basic Auth) + let authHeader; + if (token && !username) { + //如果只有 token 没有 username,使用 Bearer 认证(PAT) + authHeader = 'Bearer ' + token; + } else if (username && token) { + //如果有 username 和 token/password,使用 Basic 认证 + authHeader = 'Basic ' + btoa(username + ':' + token); + } else { + throw new Error('请填写认证信息(PAT 或 用户名+密码)'); + } + + //调用JiraAPI + const apiUrl = `${baseUrl}/rest/api/2/issue/${issueKey}`; + + const res = await fetch('/editor/api/proxy-jira', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: apiUrl, + auth: authHeader + }) + }); + + const data = await res.json(); + + if (!data.success) { + throw new Error(data.error || '无法获取工单信息'); + } + + const issue = data.data; + const sprintField = issue.fields?.customfield_10105; + + if (!sprintField || sprintField.length === 0) { + throw new Error('该工单没有关联 Sprint (customfield_10105 为空)'); + } + + //解析 Sprint 字符串,提取 id + const sprintStr = sprintField[0]; + const idMatch = sprintStr.match(/id=(\d+)/); + const nameMatch = sprintStr.match(/name=([^,\]]+)/); + + if (!idMatch) { + throw new Error('无法从 Sprint 字段中解析 ID'); + } + + extractedSprintId = idMatch[1]; + const sprintName = nameMatch ? nameMatch[1] : 'Unknown'; + extractedSprintName = sprintName; + + //显示成功结果 + resultBox.innerHTML = ` +
+ Sprint ID: ${extractedSprintId} + (${sprintName}) +
+ `; + + //显示里程碑名称输入框,并预填充Sprint名称 + milestoneBox.classList.remove('hidden'); + document.getElementById('milestoneName').value = sprintName; + + } catch (e) { + showSprintError(e.message); + extractedSprintId = null; + milestoneBox.classList.add('hidden'); + } finally { + btn.disabled = false; + btn.innerText = "提取 Sprint"; + } +} + +function showSprintError(msg) { showErrorMsg('sprintError', msg); } + +function addSprintMapping() { + const milestoneName = document.getElementById('milestoneName').value.trim(); + + if (!milestoneName) { + showSprintError('请填写里程碑名称'); + return; + } + + if (!extractedSprintId) { + showSprintError('请先提取 Sprint ID'); + return; + } + + //检查是否已存在相同的里程碑名称 + const existing = sprintMappings.find(m => m.milestone === milestoneName); + if (existing) { + if (!confirm(`里程碑 "${milestoneName}" 已存在(Sprint ID: ${existing.sprintId}),是否覆盖?`)) { + return; + } + //移除旧的 + sprintMappings = sprintMappings.filter(m => m.milestone !== milestoneName); + } + + //添加新映射 + sprintMappings.push({ + milestone: milestoneName, + sprintId: parseInt(extractedSprintId), + sprintName: extractedSprintName + }); + + //更新显示 + renderSprintList(); + + //清空输入 + document.getElementById('issueKey').value = ''; + document.getElementById('milestoneName').value = ''; + document.getElementById('sprintResult').innerHTML = ''; + document.getElementById('sprintMilestone').classList.add('hidden'); + document.getElementById('sprintError').classList.add('hidden'); + + extractedSprintId = null; + extractedSprintName = null; + + //更新预览 + updatePreview(); +} + +function renderSprintList() { + const container = document.getElementById('sprintListContainer'); + const list = document.getElementById('sprintList'); + + if (sprintMappings.length === 0) { + container.classList.add('hidden'); + return; + } + + container.classList.remove('hidden'); + list.innerHTML = sprintMappings.map((m, index) => ` +
+
+ ${m.milestone} + + Sprint ID: ${m.sprintId} + (${m.sprintName}) +
+ +
+ `).join(''); +} + +function removeSprintMapping(index) { + sprintMappings.splice(index, 1); + renderSprintList(); + updatePreview(); +} + +async function saveToFile() { + if (!currentRepoName) { + alert('错误:未设置仓库名称'); + return; + } + + if (!fetchedData) { + alert('请先扫描Jira项目信息'); + return; + } + + //尝试从JSON预览读取配置 + const jsonPreview = document.getElementById('jsonPreview'); + const jsonError = document.getElementById('jsonError'); + let config; + + if (jsonPreview.value.trim()) { + try { + config = JSON.parse(jsonPreview.value); + jsonError.classList.add('hidden'); + } catch (e) { + jsonError.textContent = `JSON格式错误: ${e.message}`; + jsonError.classList.remove('hidden'); + return; + } + } else { + //如果JSON预览为空,从表单收集配置 + config = buildConfigFromForm(); + } + + //更新JSON预览 + updatePreview(); + + //保存到服务器 + try { + const res = await fetch('/editor/api/mappings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + repoName: currentRepoName, + config: config + }) + }); + + const data = await res.json(); + + if (!data.success) { + throw new Error(data.error); + } + + //显示成功信息 + document.getElementById('saveResult').innerHTML = ` +
+

配置已成功保存!

+

仓库: ${currentRepoName}

+

文件: mappings.json

+
+ `; + + document.getElementById('step2').classList.add('hidden'); + document.getElementById('step3').classList.remove('hidden'); + document.getElementById('step3').scrollIntoView({ behavior: "smooth" }); + + } catch (e) { + alert('保存失败: ' + e.message); + } +} + +//从表单构建配置对象 +function buildConfigFromForm() { + const projectId = fetchedData.project.id; + const projectKey = fetchedData.project.key; + const defaultType = document.getElementById('defaultTypeSelect').value; + + //收集优先级(支持多个Gitea标签映射到同一个Jira优先级) + const priorities = {}; + document.querySelectorAll('input[data-type="priority"]').forEach(input => { + const val = input.value.trim(); + if (val) { + //检查是否已存在相同的Gitea标签 + if (priorities[val] && priorities[val] !== input.dataset.jiraId) { + console.warn(`警告:Gitea标签 "${val}" 被映射到多个Jira优先级,将使用最后一个`); + } + priorities[val] = input.dataset.jiraId; + } + }); + + //收集类型(支持多个Gitea标签映射到同一个Jira类型) + const types = {}; + document.querySelectorAll('input[data-type="issuetype"]').forEach(input => { + const val = input.value.trim(); + if (val) { + //检查是否已存在相同的Gitea标签 + if (types[val] && types[val] !== input.dataset.jiraId) { + console.warn(`警告:Gitea标签 "${val}" 被映射到多个Jira类型,将使用最后一个`); + } + types[val] = input.dataset.jiraId; + } + }); + + //收集流转 + const transitions = {}; + const closeId = document.getElementById('transClose').value; + const reopenId = document.getElementById('transReopen').value; + const progressId = document.getElementById('transProgress').value; + + if (closeId) transitions['close'] = closeId; + if (reopenId) transitions['reopen'] = reopenId; + if (progressId) transitions['in_progress'] = progressId; + + //生成 Sprint 配置对象 + const sprints = {}; + sprintMappings.forEach(m => { + sprints[m.milestone] = m.sprintId; + }); + + //构建配置对象 + const jiraConfig = { + projectId: projectId, + projectKey: projectKey, + sprintField: "customfield_10105" + }; + + //只在有值时添加defaultType + if (defaultType) { + jiraConfig.defaultType = defaultType; + } + + return { + jira: jiraConfig, + priorities: priorities, + types: types, + transitions: transitions, + sprints: sprints + }; +} + +//更新JSON预览 +function updatePreview() { + if (!fetchedData) return; + + const config = buildConfigFromForm(); + const jsonPreview = document.getElementById('jsonPreview'); + const jsonError = document.getElementById('jsonError'); + + try { + jsonPreview.value = JSON.stringify(config, null, 2); + jsonError.classList.add('hidden'); + } catch (e) { + jsonError.textContent = `生成预览失败: ${e.message}`; + jsonError.classList.remove('hidden'); + } +} + +async function scanJira(existingConfig = null) { + const btn = document.getElementById('scanBtn'); + const errBox = document.getElementById('scanError'); + const step2 = document.getElementById('step2'); + + const settings = getGlobalSettings(); + const baseUrl = settings.jiraUrl; + const username = settings.jiraUser; + const token = settings.jiraToken; + const projectKey = document.getElementById('projectKey').value.trim(); + + if (!baseUrl) { + showError("请先在全局设置中配置Jira连接信息"); + return; + } + + if (!projectKey) { + showError("请输入项目 Key"); + return; + } + + btn.disabled = true; + btn.classList.add('opacity-75'); + document.getElementById('scanBtnText').innerText = existingConfig ? "加载中..." : "扫描中..."; + errBox.classList.add('hidden'); + if (!existingConfig) step2.classList.add('hidden'); + + try { + const res = await fetch('/editor/api/scan', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + baseUrl, + projectKey, + auth: { username, password: token, token: token.length > 30 ? token : null } + }) + }); + + const json = await res.json(); + + if (!json.success) { + throw new Error(json.error); + } + + fetchedData = json.data; + renderMappingUI(json.data); + + if (existingConfig) { + restoreConfigToUI(existingConfig); + } + + step2.classList.remove('hidden'); + setTimeout(() => step2.scrollIntoView({ behavior: "smooth" }), 100); + + } catch (e) { + showError(e.message); + } finally { + btn.disabled = false; + btn.classList.remove('opacity-75'); + document.getElementById('scanBtnText').innerText = "开始扫描"; + } +} + +function showError(msg) { showErrorMsg('scanError', msg); } + +//手动扫描工单的 Transition +async function scanTransitions() { + const btn = document.getElementById('scanTransBtn'); + const resultBox = document.getElementById('transResult'); + const errBox = document.getElementById('transError'); + const issueKey = document.getElementById('transIssueKey').value.trim().toUpperCase(); + + const settings = getGlobalSettings(); + const baseUrl = settings.jiraUrl; + const username = settings.jiraUser; + const token = settings.jiraToken; + + if (!baseUrl) { + showTransError("请先在全局设置中配置Jira地址"); + return; + } + + if (!issueKey) { + showTransError("请输入工单 Key"); + return; + } + + btn.disabled = true; + btn.innerText = "扫描中..."; + errBox.classList.add('hidden'); + resultBox.innerHTML = ''; + + try { + //构造认证头 + let authHeader; + if (token && !username) { + authHeader = 'Bearer ' + token; + } else if (username && token) { + authHeader = 'Basic ' + btoa(username + ':' + token); + } else { + throw new Error('请填写认证信息(PAT 或 用户名+密码)'); + } + + //调用 Jira API 获取 transitions + const apiUrl = `${baseUrl}/rest/api/2/issue/${issueKey}/transitions`; + + const res = await fetch('/editor/api/proxy-jira', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: apiUrl, auth: authHeader }) + }); + + const data = await res.json(); + + if (!data.success) { + throw new Error(data.error || '无法获取流转信息'); + } + + const transitions = data.data.transitions || []; + + if (transitions.length === 0) { + resultBox.innerHTML = '该工单当前状态没有可用的流转'; + return; + } + + //追加到下拉菜单(去重) + const transSelects = ['transClose', 'transReopen', 'transProgress']; + let addedCount = 0; + + transitions.forEach(t => { + transSelects.forEach(selectId => { + const sel = document.getElementById(selectId); + //检查是否已存在(按 ID 去重) + const exists = Array.from(sel.options).some(opt => opt.value === t.id); + if (!exists) { + const opt = document.createElement('option'); + opt.value = t.id; + opt.text = `${t.name} (To: ${t.to?.name || 'Unknown'})`; + sel.add(opt); + addedCount++; + } + }); + }); + + //显示结果 + const newTransNames = transitions.map(t => `${t.name} → ${t.to?.name || '?'}`).join(', '); + resultBox.innerHTML = ` +
+ 扫描到 ${transitions.length} 个流转: ${newTransNames} +
+ `; + + //同时更新 fetchedData 以保持一致 + if (fetchedData) { + transitions.forEach(t => { + const exists = fetchedData.transitions.some(existing => existing.id === t.id); + if (!exists) { + fetchedData.transitions.push({ + id: t.id, + name: t.name, + to: t.to?.name || 'Unknown' + }); + } + }); + } + + } catch (e) { + showTransError(e.message); + } finally { + btn.disabled = false; + btn.innerText = "扫描流转"; + } +} + +function showTransError(msg) { showErrorMsg('transError', msg); } + +function renderMappingUI(data) { + //渲染优先级 + const pContainer = document.getElementById('priorityContainer'); + pContainer.innerHTML = ''; + data.priorities.forEach(p => { + let guess = ""; + for (let k in defaultLabels) { + if (p.name.includes(k)) guess = defaultLabels[k]; + } + + const itemDiv = document.createElement('div'); + itemDiv.className = 'bg-gray-50 p-3 rounded border'; + itemDiv.dataset.jiraId = p.id; + itemDiv.innerHTML = ` +
+
+ + ${p.name} +
+ +
+
+
+ Gitea标签: + + +
+
+ `; + pContainer.appendChild(itemDiv); + }); + + //渲染类型 + const tContainer = document.getElementById('typeContainer'); + const defaultSelect = document.getElementById('defaultTypeSelect'); + tContainer.innerHTML = ''; + defaultSelect.innerHTML = ''; + + //添加"不设置"选项 + const emptyOption = document.createElement('option'); + emptyOption.value = ''; + emptyOption.text = '(不设置默认类型)'; + defaultSelect.add(emptyOption); + + data.types.forEach((t, index) => { + let guess = ""; + for (let k in defaultLabels) { + if (t.name.includes(k)) guess = defaultLabels[k]; + } + + const itemDiv = document.createElement('div'); + itemDiv.className = 'bg-gray-50 p-3 rounded border'; + itemDiv.dataset.jiraId = t.id; + itemDiv.innerHTML = ` +
+
+ + ${t.name} +
+ +
+
+
+ Gitea标签: + + +
+
+ `; + tContainer.appendChild(itemDiv); + + const option = document.createElement('option'); + option.value = t.id; + option.text = t.name; + defaultSelect.add(option); + if (t.name.toLowerCase() === 'bug' || t.name.toLowerCase() === 'task') defaultSelect.value = t.id; + }); + + //渲染流转 + const transSelects = ['transClose', 'transReopen', 'transProgress']; + const warningBox = document.getElementById('transWarning'); + + if (data.warning) { + warningBox.innerText = data.warning; + warningBox.classList.remove('hidden'); + } else { + warningBox.classList.add('hidden'); + } + + transSelects.forEach(id => { + const sel = document.getElementById(id); + sel.innerHTML = ''; + data.transitions.forEach(t => { + const opt = document.createElement('option'); + opt.value = t.id; + opt.text = `${t.name} (To: ${t.to})`; + sel.add(opt); + + const lowerName = t.name.toLowerCase(); + const lowerTo = t.to.toLowerCase(); + if (id === 'transClose' && (lowerName.includes('close') || lowerName.includes('done') || lowerName.includes('complete'))) sel.value = t.id; + if (id === 'transReopen' && (lowerName.includes('reopen') || lowerName.includes('open'))) sel.value = t.id; + if (id === 'transProgress' && (lowerName.includes('progress') || lowerName.includes('process') || lowerTo.includes('progress'))) sel.value = t.id; + }); + }); + + //为所有输入框添加input事件,自动更新预览(使用input替代change以实现实时更新) + document.querySelectorAll('input[data-type]').forEach(el => { + el.addEventListener('input', updatePreview); + }); + document.querySelectorAll('select').forEach(el => { + el.addEventListener('change', updatePreview); + }); + + //初始化预览 + updatePreview(); +} \ No newline at end of file