关闭 (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