init:taskbot 1.1.0
This commit is contained in:
400
public/dashboard-app.js
Normal file
400
public/dashboard-app.js
Normal file
@@ -0,0 +1,400 @@
|
||||
//标签页切换
|
||||
function switchTab(tab) {
|
||||
//更新侧边栏按钮状态
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('bg-indigo-600', 'text-white');
|
||||
btn.classList.add('hover:bg-slate-800', 'text-slate-400', 'hover:text-white');
|
||||
});
|
||||
|
||||
const activeBtn = document.getElementById(`tab-${tab}`);
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
activeBtn.classList.remove('hover:bg-slate-800', 'text-slate-400', 'hover:text-white');
|
||||
}
|
||||
|
||||
//切换内容区
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.add('hidden');
|
||||
});
|
||||
|
||||
const activeContent = document.getElementById(`content-${tab}`);
|
||||
if (activeContent) {
|
||||
activeContent.classList.remove('hidden');
|
||||
}
|
||||
|
||||
//如果切换到日志页,开始实时加载
|
||||
if (tab === 'logs') {
|
||||
startLogStreaming();
|
||||
} else {
|
||||
stopLogStreaming();
|
||||
}
|
||||
|
||||
//如果切换到设置页,加载 .env 文件
|
||||
if (tab === 'settings') {
|
||||
loadEnvFile();
|
||||
}
|
||||
|
||||
//如果切换到使用指南页,加载 README
|
||||
if (tab === 'guide') {
|
||||
loadGuide();
|
||||
}
|
||||
}
|
||||
|
||||
//日志流控制
|
||||
let logInterval = null;
|
||||
let lastLogSize = 0;
|
||||
|
||||
async function startLogStreaming() {
|
||||
//立即加载一次
|
||||
await loadLogs();
|
||||
|
||||
//每2秒刷新一次
|
||||
logInterval = setInterval(loadLogs, 2000);
|
||||
}
|
||||
|
||||
function stopLogStreaming() {
|
||||
if (logInterval) {
|
||||
clearInterval(logInterval);
|
||||
logInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const res = await fetch('/api/logs');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
const logViewer = document.getElementById('log-viewer');
|
||||
document.getElementById('log-filename').textContent = data.filename || 'sync_service.log';
|
||||
|
||||
if (data.logs && data.logs.length > 0) {
|
||||
//只在日志有变化时更新
|
||||
const newContent = data.logs.map((log, index) =>
|
||||
`<div class="log-line border-l-2 border-transparent hover:border-slate-700 hover:bg-slate-800/50 pl-2 py-0.5">
|
||||
<span class="opacity-50 select-none mr-2">${index + 1}</span>${escapeHtml(log)}
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
if (logViewer.innerHTML !== newContent) {
|
||||
logViewer.innerHTML = newContent;
|
||||
//自动滚动到底部
|
||||
logViewer.scrollTop = logViewer.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
logViewer.innerHTML = '<div class="text-slate-500 text-center py-8">暂无日志</div>';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载日志失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
//HTML 转义
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
//加载仪表盘数据
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
const res = await fetch('/api/status');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
//更新统计数据
|
||||
document.getElementById('today-syncs').textContent = data.todaySyncs || '--';
|
||||
document.getElementById('repo-count').textContent = data.repoCount || '--';
|
||||
document.getElementById('error-count').textContent = (data.errorCount + data.fatalCount) || '--';
|
||||
document.getElementById('uptime').textContent = data.uptime ? `系统运行时间: ${data.uptime}` : '加载中...';
|
||||
|
||||
//更新服务状态
|
||||
updateServiceStatus(data.status);
|
||||
}
|
||||
|
||||
//加载历史记录
|
||||
loadHistory();
|
||||
} catch (e) {
|
||||
console.error('加载仪表盘数据失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
const res = await fetch('/api/history');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.history) {
|
||||
const tbody = document.getElementById('history-table');
|
||||
|
||||
if (data.history.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">暂无历史记录</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.history.map(day => `
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-4 py-3 text-sm text-slate-900">${day.date}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-900 text-right">${day.syncs}</td>
|
||||
<td class="px-4 py-3 text-sm ${day.errors > 0 ? 'text-rose-600 font-medium' : 'text-slate-900'} text-right">${day.errors}</td>
|
||||
<td class="px-4 py-3 text-sm ${day.fatals > 0 ? 'text-rose-700 font-bold' : 'text-slate-900'} text-right">${day.fatals}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载历史数据失败:', e);
|
||||
const tbody = document.getElementById('history-table');
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-rose-500">加载失败</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateServiceStatus(status) {
|
||||
const badge = document.getElementById('status-badge');
|
||||
const statusText = document.getElementById('service-status');
|
||||
|
||||
if (status === 'running') {
|
||||
badge.className = 'px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-emerald-50 text-emerald-600 border-emerald-200';
|
||||
badge.innerHTML = `
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
运行中
|
||||
`;
|
||||
statusText.textContent = '运行中';
|
||||
} else {
|
||||
badge.className = 'px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-slate-100 text-slate-600 border-slate-200';
|
||||
badge.innerHTML = `
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
已停止
|
||||
`;
|
||||
statusText.textContent = '已停止';
|
||||
}
|
||||
}
|
||||
|
||||
//控制机器人
|
||||
async function controlBot(action) {
|
||||
try {
|
||||
const res = await fetch('/api/control', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
alert(`操作成功: ${data.message || action}`);
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert(`操作失败: ${data.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`操作失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
//清空日志
|
||||
async function clearLogs() {
|
||||
if (!confirm('确定要清空日志吗?此操作不可恢复。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/logs/clear', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('日志已清空');
|
||||
if (document.getElementById('content-logs').classList.contains('hidden') === false) {
|
||||
loadLogs();
|
||||
}
|
||||
} else {
|
||||
alert(`清空失败: ${data.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`清空失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
//刷新状态
|
||||
function refreshStatus() {
|
||||
loadDashboardData();
|
||||
}
|
||||
|
||||
async function loadEnvFile() {
|
||||
const container = document.getElementById('envEditor');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/env');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
const envContent = data.content;
|
||||
const envItems = parseEnvContent(envContent);
|
||||
renderEnvForm(envItems);
|
||||
} else {
|
||||
container.innerHTML = `<div class="text-center py-8 text-rose-400">${data.error}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
container.innerHTML = `<div class="text-center py-8 text-rose-400">加载失败: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function parseEnvContent(content) {
|
||||
const lines = content.split('\n');
|
||||
const items = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed === '' || trimmed.startsWith('#')) {
|
||||
items.push({ type: 'comment', value: line });
|
||||
} else if (trimmed.includes('=')) {
|
||||
const equalIndex = trimmed.indexOf('=');
|
||||
const key = trimmed.substring(0, equalIndex).trim();
|
||||
const value = trimmed.substring(equalIndex + 1).trim();
|
||||
items.push({ type: 'var', key, value });
|
||||
} else {
|
||||
items.push({ type: 'comment', value: line });
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function renderEnvForm(items) {
|
||||
const container = document.getElementById('envEditor');
|
||||
container.innerHTML = items.map((item, index) => {
|
||||
if (item.type === 'comment') {
|
||||
return `<div class="text-slate-400 text-xs font-mono py-1">${escapeHtml(item.value)}</div>`;
|
||||
} else {
|
||||
const isSecret = item.key.toLowerCase().includes('token') ||
|
||||
item.key.toLowerCase().includes('secret') ||
|
||||
item.key.toLowerCase().includes('password') ||
|
||||
item.key.toLowerCase().includes('pat');
|
||||
return `
|
||||
<div class="flex items-center gap-3 bg-slate-50 hover:bg-slate-100 p-3 rounded border border-slate-200 transition-colors">
|
||||
<label class="text-slate-700 font-mono text-sm flex-shrink-0 w-48 font-medium">${escapeHtml(item.key)}</label>
|
||||
<span class="text-slate-400">=</span>
|
||||
<input type="${isSecret ? 'password' : 'text'}"
|
||||
data-key="${escapeHtml(item.key)}"
|
||||
value="${escapeHtml(item.value)}"
|
||||
class="flex-1 bg-white border border-slate-300 rounded px-3 py-2 text-slate-900 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
placeholder="未设置">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function saveEnvFile() {
|
||||
const btn = document.getElementById('saveEnvBtn');
|
||||
|
||||
if (!confirm('确定要保存配置吗?保存后需要重启服务才能生效。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = '保存中...';
|
||||
|
||||
try {
|
||||
const content = buildEnvContent();
|
||||
|
||||
const res = await fetch('/api/env', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
alert(`保存成功!\n\n${data.message}`);
|
||||
loadEnvFile();
|
||||
} else {
|
||||
alert(`保存失败: ${data.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`保存失败: ${e.message}`);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '保存配置';
|
||||
}
|
||||
}
|
||||
|
||||
function buildEnvContent() {
|
||||
const container = document.getElementById('envEditor');
|
||||
const lines = [];
|
||||
|
||||
container.childNodes.forEach(node => {
|
||||
if (node.classList && node.classList.contains('text-slate-400')) {
|
||||
lines.push(node.textContent);
|
||||
} else if (node.classList && node.classList.contains('bg-slate-50')) {
|
||||
const input = node.querySelector('input');
|
||||
const key = input.dataset.key;
|
||||
const value = input.value;
|
||||
lines.push(`${key}=${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function loadGuide() {
|
||||
const container = document.getElementById('guide-content');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/guide');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.content) {
|
||||
//使用marked.js渲染markdown
|
||||
if (typeof marked !== 'undefined') {
|
||||
container.innerHTML = marked.parse(data.content);
|
||||
} else {
|
||||
//如果marked未加载,显示原始文本
|
||||
container.innerHTML = `<pre>${data.content}</pre>`;
|
||||
}
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8 text-slate-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<p class="font-medium">无法加载使用指南</p>
|
||||
<p class="text-sm mt-1">${data.error || '未知错误'}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载使用指南失败:', e);
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8 text-rose-500">
|
||||
<svg class="w-12 h-12 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<p class="font-medium">加载失败</p>
|
||||
<p class="text-sm mt-1">${e.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
//页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
//默认显示dashboard
|
||||
switchTab('dashboard');
|
||||
|
||||
//加载初始数据
|
||||
loadDashboardData();
|
||||
|
||||
//定期刷新仪表盘数据(每30秒)
|
||||
setInterval(loadDashboardData, 30000);
|
||||
});
|
||||
296
public/dashboard.html
Normal file
296
public/dashboard.html
Normal file
@@ -0,0 +1,296 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TaskBot控制台</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<style>
|
||||
body { font-family: 'Inter', system-ui, sans-serif; }
|
||||
.log-line { font-family: 'Monaco', 'Courier New', monospace; }
|
||||
.status-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: .5; }
|
||||
}
|
||||
.markdown-body { line-height: 1.6; }
|
||||
.markdown-body h1 { font-size: 2em; font-weight: bold; margin-top: 1em; margin-bottom: 0.5em; }
|
||||
.markdown-body h2 { font-size: 1.5em; font-weight: bold; margin-top: 1em; margin-bottom: 0.5em; }
|
||||
.markdown-body h3 { font-size: 1.25em; font-weight: bold; margin-top: 0.8em; margin-bottom: 0.4em; }
|
||||
.markdown-body p { margin-bottom: 1em; }
|
||||
.markdown-body ul, .markdown-body ol { margin-left: 2em; margin-bottom: 1em; }
|
||||
.markdown-body li { margin-bottom: 0.5em; }
|
||||
.markdown-body code { background: #f3f4f6; padding: 0.2em 0.4em; border-radius: 3px; font-family: monospace; }
|
||||
.markdown-body pre { background: #1e293b; color: #e2e8f0; padding: 1em; border-radius: 0.5em; overflow-x: auto; margin-bottom: 1em; }
|
||||
.markdown-body pre code { background: transparent; padding: 0; }
|
||||
.markdown-body a { color: #3b82f6; text-decoration: underline; }
|
||||
.markdown-body blockquote { border-left: 4px solid #e5e7eb; padding-left: 1em; color: #6b7280; margin-bottom: 1em; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-50 text-slate-900">
|
||||
<div class="flex">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="w-64 bg-slate-900 text-slate-300 flex flex-col h-screen fixed left-0 top-0 border-r border-slate-800">
|
||||
<div class="h-16 flex items-center px-6 border-b border-slate-800 bg-slate-950">
|
||||
<svg class="w-6 h-6 text-indigo-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span class="font-bold text-white tracking-tight">TaskBot控制台</span>
|
||||
</div>
|
||||
|
||||
<nav class="flex-1 py-6 px-3 space-y-1">
|
||||
<button onclick="switchTab('dashboard')" id="tab-dashboard" class="tab-btn w-full flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors duration-150 group">
|
||||
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
运维概览
|
||||
</button>
|
||||
<button onclick="switchTab('logs')" id="tab-logs" class="tab-btn w-full flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors duration-150 group">
|
||||
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
运行日志
|
||||
</button>
|
||||
<button onclick="switchTab('mapping')" id="tab-mapping" class="tab-btn w-full flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors duration-150 group">
|
||||
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
映射配置
|
||||
</button>
|
||||
<button onclick="switchTab('settings')" id="tab-settings" class="tab-btn w-full flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors duration-150 group">
|
||||
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
系统设置
|
||||
</button>
|
||||
<button onclick="switchTab('guide')" id="tab-guide" class="tab-btn w-full flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors duration-150 group">
|
||||
<svg class="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
|
||||
</svg>
|
||||
使用指南
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="p-4 border-t border-slate-800">
|
||||
<div class="flex items-center">
|
||||
<div class="w-2 h-2 rounded-full bg-emerald-500 mr-2 status-pulse"></div>
|
||||
<span class="text-xs font-mono text-slate-500">v1.1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="flex-1 ml-64 p-8 overflow-y-auto">
|
||||
<!-- Dashboard 标签页 -->
|
||||
<div id="content-dashboard" class="tab-content">
|
||||
<header class="mb-8 flex justify-between items-end">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">运维概览</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Jira-Gitea双向同步机器人控制中心</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
|
||||
<span class="text-sm font-medium text-slate-600" id="uptime">加载中...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- 指标栏 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<div class="bg-white p-5 rounded-lg border border-slate-200 shadow-sm">
|
||||
<p class="text-sm font-medium text-slate-500">今日同步工单</p>
|
||||
<p class="mt-1 text-2xl font-bold text-slate-900 tracking-tight" id="today-syncs">--</p>
|
||||
<p class="mt-1 text-xs text-slate-400">实时统计</p>
|
||||
</div>
|
||||
<div class="bg-white p-5 rounded-lg border border-slate-200 shadow-sm">
|
||||
<p class="text-sm font-medium text-slate-500">配置的仓库</p>
|
||||
<p class="mt-1 text-2xl font-bold text-slate-900 tracking-tight" id="repo-count">--</p>
|
||||
<p class="mt-1 text-xs text-slate-400">mappings.json</p>
|
||||
</div>
|
||||
<div class="bg-white p-5 rounded-lg border border-slate-200 shadow-sm">
|
||||
<p class="text-sm font-medium text-slate-500">今日错误</p>
|
||||
<p class="mt-1 text-2xl font-bold text-rose-600 tracking-tight" id="error-count">--</p>
|
||||
<p class="mt-1 text-xs text-slate-400">ERROR + FATAL</p>
|
||||
</div>
|
||||
<div class="bg-white p-5 rounded-lg border border-slate-200 shadow-sm">
|
||||
<p class="text-sm font-medium text-slate-500">服务状态</p>
|
||||
<p class="mt-1 text-2xl font-bold text-slate-900 tracking-tight" id="service-status">运行中</p>
|
||||
<p class="mt-1 text-xs text-slate-400">实时监控</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<div class="bg-white rounded-lg border border-slate-200 shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold text-slate-900 uppercase tracking-wider mb-4">近7日同步历史</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">日期</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">同步数</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">错误数</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-medium text-slate-500 uppercase tracking-wider">严重错误</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="history-table" class="divide-y divide-slate-200">
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制区 -->
|
||||
<div class="bg-white rounded-lg border border-slate-200 shadow-sm p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-sm font-semibold text-slate-900 uppercase tracking-wider">服务控制</h3>
|
||||
<span id="status-badge" class="px-3 py-1 rounded-full text-xs font-medium flex items-center border bg-emerald-50 text-emerald-600 border-emerald-200">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
运行中
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<button onclick="controlBot('restart')" class="flex items-center justify-center px-4 py-2 border border-slate-300 text-sm font-medium rounded-md text-slate-700 bg-white hover:bg-slate-50 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
重启服务
|
||||
</button>
|
||||
<button onclick="clearLogs()" class="flex items-center justify-center px-4 py-2 border border-slate-300 text-sm font-medium rounded-md text-slate-700 bg-white hover:bg-slate-50 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
清空日志
|
||||
</button>
|
||||
<button onclick="refreshStatus()" class="flex items-center justify-center px-4 py-2 border border-slate-300 text-sm font-medium rounded-md text-slate-700 bg-white hover:bg-slate-50 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
刷新状态
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs 标签页 -->
|
||||
<div id="content-logs" class="tab-content hidden">
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">运行日志</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">实时监控系统运行状态</p>
|
||||
</header>
|
||||
|
||||
<div class="bg-slate-900 rounded-lg shadow-sm border border-slate-800 overflow-hidden" style="height: calc(100vh - 200px);">
|
||||
<div class="bg-slate-950 px-4 py-2 border-b border-slate-800 flex justify-between items-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span class="text-xs font-mono text-slate-300" id="log-filename">加载中...</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="w-2 h-2 rounded-full bg-emerald-500 status-pulse"></span>
|
||||
<span class="text-xs text-slate-500">实时监测中</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="log-viewer" class="overflow-y-auto p-4 font-mono text-xs text-slate-300 space-y-1" style="height: calc(100% - 44px);">
|
||||
<div class="text-slate-500 text-center py-8">加载日志中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mapping 标签页 - 嵌入原有的映射编辑器 -->
|
||||
<div id="content-mapping" class="tab-content hidden">
|
||||
<iframe src="/editor/error.html?code=施工中" class="w-full border-0 rounded-lg shadow-sm bg-white" style="height: calc(100vh - 100px);"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- Settings 标签页 -->
|
||||
<div id="content-settings" class="tab-content hidden">
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">系统设置</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">编辑环境变量配置文件 (.env)</p>
|
||||
</header>
|
||||
|
||||
<div class="bg-white rounded-lg border border-slate-200 shadow-sm p-6">
|
||||
<div class="mb-6 flex justify-between items-center border-b border-slate-200 pb-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-900">环境变量配置</h3>
|
||||
<p class="text-xs text-slate-500 mt-1">修改后需要重启服务才能生效</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="loadEnvFile()" class="text-slate-600 hover:text-slate-900 px-3 py-2 rounded text-sm font-medium transition-colors border border-slate-300 hover:bg-slate-50">
|
||||
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
重新加载
|
||||
</button>
|
||||
<button onclick="saveEnvFile()" id="saveEnvBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors">
|
||||
保存配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="envEditor" class="space-y-3" style="max-height: 500px; overflow-y: auto;">
|
||||
<div class="text-center py-8 text-slate-400">
|
||||
<svg class="w-8 h-8 mx-auto mb-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<p class="text-sm">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 bg-amber-50 border border-amber-200 rounded p-3 text-sm text-amber-800">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 mr-2 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-medium">注意事项</p>
|
||||
<ul class="mt-1 text-xs space-y-1">
|
||||
<li>• 修改前会自动备份为 .env.backup</li>
|
||||
<li>• 保存后需要手动重启服务才能生效</li>
|
||||
<li>• 请勿泄露敏感信息(API Token、密码等)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Guide 标签页 -->
|
||||
<div id="content-guide" class="tab-content hidden">
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">使用指南</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">项目文档与配置说明</p>
|
||||
</header>
|
||||
|
||||
<div class="bg-white rounded-lg border border-slate-200 shadow-sm p-6">
|
||||
<div id="guide-content" class="markdown-body prose max-w-none text-slate-700">
|
||||
<div class="text-center py-8 text-slate-500">
|
||||
<svg class="w-8 h-8 mx-auto mb-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/editor/dashboard-app.js"></script>
|
||||
<script>
|
||||
//页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
//默认显示运维概览
|
||||
switchTab('dashboard');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
268
public/editor.html
Normal file
268
public/editor.html
Normal file
@@ -0,0 +1,268 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gitea-Jira 映射配置生成器</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { font-family: 'Inter', system-ui, sans-serif; }
|
||||
.step-card { transition: all 0.3s ease; }
|
||||
.step-active { border-left: 4px solid #3b82f6; }
|
||||
.jira-icon { width: 16px; height: 16px; display: inline-block; vertical-align: middle; margin-right: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-800 min-h-screen p-6">
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<header class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Gitea-Jira 映射生成器</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">连接 Jira,自动提取 ID,生成 mappings.json 配置</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="openSettings()" class="text-sm bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded transition">
|
||||
全局设置
|
||||
</button>
|
||||
<div class="text-xs bg-blue-100 text-blue-800 px-3 py-1 rounded-full">v2.0</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 全局设置模态窗口 -->
|
||||
<div id="settingsModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl p-6 max-w-lg w-full mx-4">
|
||||
<h2 class="text-xl font-bold mb-4">全局设置</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Jira 地址 (Base URL)</label>
|
||||
<input type="text" id="settingsJiraUrl" placeholder="https://your-domain.atlassian.net" class="w-full border rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Jira 用户名/邮箱</label>
|
||||
<input type="text" id="settingsJiraUser" placeholder="email@example.com" class="w-full border rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<p class="text-xs text-gray-400 mt-1">使用 PAT 时可留空</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">API Token / 密码</label>
|
||||
<input type="password" id="settingsJiraToken" placeholder="ATATT3xFfGF0..." class="w-full border rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<p class="text-xs text-gray-400 mt-1">推荐使用 PAT (Personal Access Token)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<button onclick="closeSettings()" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded text-sm font-medium transition">
|
||||
取消
|
||||
</button>
|
||||
<button onclick="saveSettings()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition">
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 改名模态窗口 -->
|
||||
<div id="renameModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl p-6 max-w-lg w-full mx-4">
|
||||
<h2 class="text-xl font-bold mb-4">重命名仓库</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">当前名称</label>
|
||||
<input type="text" id="renameOldName" readonly class="w-full border rounded px-3 py-2 text-sm bg-gray-100 text-gray-600">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">新名称</label>
|
||||
<input type="text" id="renameNewName" placeholder="例如: owner/repo" class="w-full border rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<p class="text-xs text-gray-400 mt-1">格式: owner/repo</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="renameError" class="hidden mt-3 bg-red-50 text-red-700 p-3 rounded text-sm border border-red-200"></div>
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
<button onclick="closeRenameModal()" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded text-sm font-medium transition">
|
||||
取消
|
||||
</button>
|
||||
<button onclick="confirmRename()" id="renameConfirmBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition">
|
||||
确认改名
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 0: 选择/新建仓库 -->
|
||||
<div id="step0" class="bg-white rounded-lg shadow p-6 mb-6 step-card step-active">
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center">
|
||||
<span class="bg-blue-100 text-blue-600 rounded-full w-8 h-8 flex items-center justify-center text-sm mr-3">0</span>
|
||||
选择仓库配置
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<button onclick="loadExistingMappings()" id="loadBtn" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded text-sm font-medium transition">
|
||||
加载现有配置
|
||||
</button>
|
||||
<button onclick="createNewMapping()" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded text-sm font-medium transition ml-2">
|
||||
新建仓库配置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="repoListContainer" class="hidden">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">现有仓库配置:</h3>
|
||||
<div id="repoList" class="space-y-2 max-h-64 overflow-y-auto"></div>
|
||||
</div>
|
||||
|
||||
<div id="newRepoContainer" class="hidden">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">新仓库名称</label>
|
||||
<input type="text" id="newRepoName" placeholder="例如: loren/issueBotTest" class="w-full border rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<button onclick="confirmNewRepo()" class="mt-2 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition">
|
||||
确认并继续
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="step0Error" class="hidden mt-3 bg-red-50 text-red-700 p-3 rounded text-sm border border-red-200"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: 项目配置 -->
|
||||
<div id="step1" class="bg-white rounded-lg shadow p-6 mb-6 step-card hidden">
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center">
|
||||
<span class="bg-blue-100 text-blue-600 rounded-full w-8 h-8 flex items-center justify-center text-sm mr-3">1</span>
|
||||
项目配置
|
||||
</h2>
|
||||
<div class="mb-3 bg-blue-50 border border-blue-200 rounded p-3 flex items-center justify-between">
|
||||
<p class="text-sm text-blue-800">
|
||||
<span class="font-semibold">当前配置仓库:</span> <span id="currentRepoName" class="font-mono">-</span>
|
||||
</p>
|
||||
<button onclick="backToStart()" class="text-xs bg-white hover:bg-gray-50 text-gray-700 px-3 py-1 rounded border">
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">目标 Jira 项目 Key</label>
|
||||
<input type="text" id="projectKey" placeholder="例如: TEST" class="w-full border rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none uppercase font-bold text-gray-700">
|
||||
</div>
|
||||
|
||||
<!-- Sprint ID 提取区域 -->
|
||||
<div class="mt-4 border-t pt-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">配置 Sprint 映射(可选)</h3>
|
||||
<p class="text-xs text-gray-500 mb-3">输入一个已在目标 Sprint 中的工单 Key,系统将自动提取 Sprint ID 并建立映射</p>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="issueKey" placeholder="例如: TEST-196" class="w-1/5 border rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none uppercase">
|
||||
<button onclick="fetchSprintId()" id="fetchSprintBtn" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded text-sm font-medium transition whitespace-nowrap">
|
||||
提取 Sprint
|
||||
</button>
|
||||
<div id="sprintResult" class="flex-1 flex items-center px-3 text-sm"></div>
|
||||
</div>
|
||||
<div id="sprintMilestone" class="hidden mt-3 flex gap-2 items-center">
|
||||
<label class="text-xs font-medium text-gray-600 whitespace-nowrap">里程碑名称:</label>
|
||||
<input type="text" id="milestoneName" placeholder="例如: v1.0.1" class="flex-1 border rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none">
|
||||
<button onclick="addSprintMapping()" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded text-sm font-medium transition whitespace-nowrap">
|
||||
添加映射
|
||||
</button>
|
||||
</div>
|
||||
<div id="sprintError" class="hidden mt-2 bg-red-50 text-red-700 p-2 rounded text-xs border border-red-200"></div>
|
||||
|
||||
<!-- Sprint 映射列表 -->
|
||||
<div id="sprintListContainer" class="hidden mt-4">
|
||||
<h4 class="text-xs font-semibold text-gray-600 mb-2">已添加的 Sprint 映射:</h4>
|
||||
<div id="sprintList" class="space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button onclick="scanJira()" id="scanBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded shadow text-sm font-medium transition flex items-center">
|
||||
<span id="scanBtnText">开始扫描</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="scanError" class="hidden mt-3 bg-red-50 text-red-700 p-3 rounded text-sm border border-red-200"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: 交互映射 -->
|
||||
<div id="step2" class="bg-white rounded-lg shadow p-6 mb-6 hidden">
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center">
|
||||
<span class="bg-blue-100 text-blue-600 rounded-full w-8 h-8 flex items-center justify-center text-sm mr-3">2</span>
|
||||
配置映射关系
|
||||
</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- 优先级 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-gray-800 uppercase tracking-wide border-b pb-2 mb-3">优先级映射 (Priority)</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="priorityContainer">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单类型 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-gray-800 uppercase tracking-wide border-b pb-2 mb-3">类型映射 (不填写的类型不会同步)(Issue Types)</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="typeContainer">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-600 flex items-center gap-2">
|
||||
<label>默认类型(设置后即使Gitea没打标签也会被同步到Jira):</label>
|
||||
<select id="defaultTypeSelect" class="border rounded px-2 py-1 text-sm bg-gray-50"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态流转 -->
|
||||
<div>
|
||||
<h3 class="text-sm font-bold text-gray-800 uppercase tracking-wide border-b pb-2 mb-3">状态动作 (Transitions)</h3>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded p-3 mb-3 text-xs text-yellow-800 hidden" id="transWarning"></div>
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div class="flex items-center justify-between bg-gray-50 p-3 rounded border">
|
||||
<span class="font-mono text-sm font-medium text-gray-700">关闭 (Close)</span>
|
||||
<select id="transClose" class="border rounded px-2 py-1 text-sm w-1/2"></select>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-gray-50 p-3 rounded border">
|
||||
<span class="font-mono text-sm font-medium text-gray-700">重开 (Reopen)</span>
|
||||
<select id="transReopen" class="border rounded px-2 py-1 text-sm w-1/2"></select>
|
||||
</div>
|
||||
<div class="flex items-center justify-between bg-gray-50 p-3 rounded border">
|
||||
<span class="font-mono text-sm font-medium text-gray-700">处理中 (In Progress)</span>
|
||||
<select id="transProgress" class="border rounded px-2 py-1 text-sm w-1/2"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JSON 预览和编辑区 -->
|
||||
<div class="mt-6 border-t pt-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-bold text-gray-800 uppercase tracking-wide">配置预览(可编辑)</h3>
|
||||
<button onclick="updatePreview()" class="text-xs bg-blue-50 hover:bg-blue-100 text-blue-600 px-3 py-1 rounded transition">
|
||||
刷新预览
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<textarea id="jsonPreview" class="w-full h-96 bg-gray-900 text-gray-100 font-mono text-xs p-4 rounded outline-none border-2 border-gray-700 focus:border-blue-500 transition" spellcheck="false" placeholder="配置将在此显示,可直接编辑..."></textarea>
|
||||
<div id="jsonError" class="hidden mt-2 bg-red-50 text-red-700 p-2 rounded text-xs border border-red-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end border-t pt-4">
|
||||
<button onclick="saveToFile()" class="bg-green-600 hover:bg-green-700 text-white px-6 py-2 rounded shadow text-sm font-medium transition">
|
||||
保存到 mappings.json
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: 保存结果 -->
|
||||
<div id="step3" class="bg-white rounded-lg shadow p-6 mb-12 hidden">
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="bg-green-100 text-green-600 rounded-full w-8 h-8 flex items-center justify-center text-sm mr-3">3</span>
|
||||
保存成功
|
||||
</div>
|
||||
</h2>
|
||||
<div id="saveResult" class="text-sm"></div>
|
||||
<div class="mt-4">
|
||||
<button onclick="location.reload()" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition">
|
||||
继续配置其他仓库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
61
public/error.html
Normal file
61
public/error.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>错误 - TaskBot</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body { font-family: 'Inter', system-ui, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-50 flex items-center justify-center">
|
||||
<div class="max-w-md w-full px-6">
|
||||
<div class="text-center">
|
||||
<div class="mb-6">
|
||||
<svg class="w-24 h-24 mx-auto text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="text-6xl font-bold text-slate-900 mb-2" id="error-code">404</h1>
|
||||
<h2 class="text-xl font-medium text-slate-700 mb-4" id="error-title">页面未找到</h2>
|
||||
<p class="text-sm text-slate-500 mb-8" id="error-message">抱歉,您访问的页面不存在或已被移除。</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
<button onclick="window.parent.switchTab('dashboard')" class="block w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-3 px-6 rounded-lg transition-colors">
|
||||
返回控制台
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 text-center">
|
||||
<p class="text-xs text-slate-400">TaskBot v1.1.0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get('code') || '404';
|
||||
const title = params.get('title') || '';
|
||||
const message = params.get('message') || '';
|
||||
|
||||
const errorMap = {
|
||||
'400': { title: '请求错误', message: '请求参数有误,请检查后重试。' },
|
||||
'401': { title: '未授权', message: '您没有权限访问此资源,请先登录。' },
|
||||
'403': { title: '禁止访问', message: '您没有权限访问此页面。' },
|
||||
'404': { title: '页面未找到', message: '抱歉,您访问的页面不存在或已被移除。' },
|
||||
'500': { title: '服务器错误', message: '服务器遇到了一些问题,请稍后再试。' },
|
||||
'503': { title: '服务不可用', message: '服务暂时不可用,请稍后再试。' },
|
||||
'施工中': { title: '图形化编辑映射功能暂不可用', message: '该功能预计2026/01/30下午上线' }
|
||||
};
|
||||
|
||||
const error = errorMap[code] || errorMap['404'];
|
||||
|
||||
document.getElementById('error-code').textContent = code;
|
||||
document.getElementById('error-title').textContent = title || error.title;
|
||||
document.getElementById('error-message').textContent = message || error.message;
|
||||
document.title = `${code} ${error.title} - TaskBot`;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user