Files
task-board/public/index-debug.html
T

2958 lines
96 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多智能体任务管理系统 v3.0</title>
<!-- 第三方库 CDN -->
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.1/Sortable.min.js"></script>
<style>
/* ==================== CSS 设计系统 ==================== */
:root {
/* 主题色 */
--primary: #667eea;
--primary-dark: #764ba2;
/* 状态色 */
--success: #67C23A;
--warning: #E6A23C;
--danger: #F56C6C;
--info: #909399;
/* 任务状态色 */
--status-initial: #3498db;
--status-in-progress: #f39c12;
--status-testing-pending: #9b59b6;
--status-testing: #e67e22;
--status-completed: #27ae60;
/* 任务类型色 */
--type-requirement: #9C27B0;
--type-design: #2196F3;
--type-development: #4CAF50;
--type-testing: #FF9800;
--type-deployment: #F44336;
/* 灰度色 */
--text-primary: #303133;
--text-regular: #606266;
--text-secondary: #909399;
--text-placeholder: #C0C4CC;
--border-base: #DCDFE6;
--border-light: #E4E7ED;
--border-lighter: #EBEEF5;
--border-extra-light: #F2F6FC;
--bg-base: #FFFFFF;
--bg-light: #F5F7FA;
--bg-dark: #F0F2F5;
/* 间距系统 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-xxl: 48px;
/* 圆角 */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
/* 阴影 */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.12);
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.15);
/* 字体 */
--font-family-base: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
--font-family-mono: 'Consolas', 'Monaco', 'Courier New', monospace;
--font-size-xs: 12px;
--font-size-sm: 13px;
--font-size-md: 14px;
--font-size-lg: 16px;
--font-size-xl: 18px;
--font-size-xxl: 20px;
}
/* ==================== Reset & Base Styles ==================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family-base);
font-size: var(--font-size-md);
color: var(--text-primary);
background: var(--bg-light);
line-height: 1.6;
}
button {
font-family: inherit;
cursor: pointer;
border: none;
outline: none;
}
input, textarea, select {
font-family: inherit;
border: 1px solid var(--border-base);
outline: none;
}
input:focus, textarea:focus, select:focus {
border-color: var(--primary);
}
/* ==================== 布局样式 ==================== */
#app {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.app-body {
display: flex;
flex: 1;
overflow: hidden;
}
/* ==================== Header 样式 ==================== */
#header {
height: 60px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-lg);
box-shadow: var(--shadow-md);
z-index: 100;
}
.mobile-menu-btn {
display: none;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: 20px;
cursor: pointer;
margin-right: var(--spacing-md);
transition: background 0.2s;
flex-shrink: 0;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.mobile-menu-btn:active {
background: rgba(255, 255, 255, 0.4);
}
.header-left {
display: flex;
align-items: center;
}
.header-left h1 {
font-size: var(--font-size-xl);
font-weight: 600;
}
.header-center {
display: flex;
gap: var(--spacing-lg);
}
.stat-mini {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: rgba(255, 255, 255, 0.2);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
}
.stat-mini span {
font-weight: 700;
font-size: var(--font-size-lg);
}
.header-right {
display: flex;
gap: var(--spacing-md);
}
.header-right button {
padding: var(--spacing-sm) var(--spacing-md);
background: rgba(255, 255, 255, 0.2);
color: white;
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
transition: background 0.2s;
}
.header-right button:hover {
background: rgba(255, 255, 255, 0.3);
}
/* ==================== Sidebar 样式 ==================== */
#sidebar {
width: 240px;
background: white;
border-right: 1px solid var(--border-light);
display: flex;
flex-direction: column;
overflow-y: auto;
transition: width 0.3s, transform 0.3s;
}
#sidebar.collapsed {
width: 60px;
}
#sidebar.collapsed .section-title,
#sidebar.collapsed .agent-summary,
#sidebar.collapsed .project-item span,
#sidebar.collapsed .project-delete-btn {
display: none;
}
#sidebar.collapsed .project-item {
justify-content: center;
padding: var(--spacing-sm);
}
#sidebar.collapsed .project-item::before {
content: '📁';
font-size: 20px;
}
.sidebar-header {
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-md);
border-bottom: 1px solid var(--border-light);
}
#btn-collapse {
width: 32px;
height: 32px;
background: var(--bg-light);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-lg);
transition: background 0.2s;
}
#btn-collapse:hover {
background: var(--border-light);
}
.sidebar-section {
padding: var(--spacing-md);
border-bottom: 1px solid var(--border-lighter);
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.section-title button {
width: 24px;
height: 24px;
background: var(--primary);
color: white;
border-radius: var(--radius-sm);
font-size: 16px;
line-height: 1;
}
#project-list {
list-style: none;
}
.project-item {
padding: var(--spacing-sm) var(--spacing-md);
margin-bottom: var(--spacing-xs);
border-radius: var(--radius-md);
cursor: pointer;
transition: background 0.2s;
font-size: var(--font-size-sm);
color: var(--text-regular);
}
.project-item:hover {
background: var(--bg-light);
}
.project-item.active {
background: var(--primary);
color: white;
font-weight: 600;
}
.project-delete-btn {
display: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(245, 108, 108, 0.2);
color: var(--danger);
border: none;
cursor: pointer;
font-size: 16px;
line-height: 1;
transition: all 0.2s;
flex-shrink: 0;
}
.project-delete-btn:hover {
background: var(--danger);
color: white;
transform: scale(1.1);
}
.project-item:hover .project-delete-btn {
display: flex;
align-items: center;
justify-content: center;
}
.project-item.active .project-delete-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.project-item.active:hover .project-delete-btn {
background: rgba(255, 255, 255, 0.9);
color: var(--danger);
}
.agent-summary {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
color: var(--text-regular);
}
.agent-summary span {
font-weight: 700;
color: var(--text-primary);
}
/* 设置菜单样式 */
.settings-menu {
list-style: none;
padding: 0;
margin: 0;
}
.settings-menu .nav-item {
padding: 6px 12px;
margin-bottom: 6px;
cursor: pointer;
border-radius: 6px;
transition: background 0.2s;
font-size: 13px;
text-align: left;
}
.settings-menu .nav-item:hover {
background: rgba(102, 126, 234, 0.1);
}
/* ==================== Main Content 样式 ==================== */
#main-content {
flex: 1;
overflow: hidden;
padding: var(--spacing-lg);
position: relative;
display: flex;
flex-direction: column;
}
#content-task-board,
#content-agents,
#content-rules {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#content-empty {
flex: 1;
}
.hidden {
display: none !important;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
font-size: var(--font-size-lg);
}
.empty-state p {
margin-top: var(--spacing-md);
}
#content-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--border-light);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ==================== Kanban 看板样式 ==================== */
#kanban-board {
height: 100%;
display: flex;
flex-direction: column;
}
.kanban-header {
display: flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
flex-wrap: wrap;
}
.kanban-header button {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--primary);
color: white;
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
transition: opacity 0.2s;
}
.kanban-header button:hover {
opacity: 0.9;
}
.kanban-header input, .kanban-header select {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
}
.kanban-header input {
flex: 1;
min-width: 200px;
}
.kanban-columns {
display: flex;
gap: var(--spacing-md);
flex: 1;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: var(--spacing-md);
min-height: 0;
}
.kanban-column {
flex: 1;
min-width: 280px;
background: var(--bg-dark);
border-radius: var(--radius-lg);
padding: var(--spacing-md);
display: flex;
flex-direction: column;
transition: background 0.2s, border 0.2s;
height: 100%;
}
.kanban-column.drag-over {
background: rgba(102, 126, 234, 0.05);
border: 2px dashed var(--primary);
}
.kanban-column h3 {
font-size: var(--font-size-md);
margin-bottom: var(--spacing-md);
display: flex;
align-items: center;
justify-content: space-between;
}
.kanban-column h3 .count {
background: white;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 600;
}
/* 状态列颜色 */
.kanban-column[data-status="initial"] h3 { color: var(--status-initial); }
.kanban-column[data-status="in_progress"] h3 { color: var(--status-in-progress); }
.kanban-column[data-status="testing_pending"] h3 { color: var(--status-testing-pending); }
.kanban-column[data-status="testing"] h3 { color: var(--status-testing); }
.kanban-column[data-status="completed"] h3 { color: var(--status-completed); }
.task-list {
flex: 1;
overflow-y: auto;
min-height: 100px;
}
/* ==================== 任务卡片样式 ==================== */
.task-card {
background: white;
border-radius: var(--radius-md);
padding: var(--spacing-md);
margin-bottom: var(--spacing-sm);
box-shadow: var(--shadow-sm);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.task-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.task-card.dragging {
opacity: 0.5;
transform: rotate(3deg);
box-shadow: var(--shadow-lg);
}
.task-title {
font-weight: 600;
font-size: var(--font-size-md);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
line-height: 1.4;
}
.task-meta {
display: flex;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
flex-wrap: wrap;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 600;
color: white;
}
/* 任务类型徽章 */
.badge.type-requirement { background: var(--type-requirement); }
.badge.type-design { background: var(--type-design); }
.badge.type-development { background: var(--type-development); }
.badge.type-testing { background: var(--type-testing); }
.badge.type-deployment { background: var(--type-deployment); }
/* 优先级徽章 */
.badge.priority-high { background: var(--danger); }
.badge.priority-normal { background: var(--info); }
.badge.priority-low { background: var(--text-placeholder); }
.task-footer {
display: flex;
align-items: center;
justify-content: space-between;
font-size: var(--font-size-xs);
color: var(--text-secondary);
}
.assignee {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.task-icons {
display: flex;
gap: var(--spacing-sm);
}
/* ==================== 任务详情侧边栏样式 ==================== */
#task-detail-sidebar {
position: fixed;
right: 0;
top: 60px;
width: 480px;
height: calc(100vh - 60px);
background: white;
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.15);
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
overflow-y: auto;
}
#task-detail-sidebar.active {
transform: translateX(0);
}
.sidebar-header-detail {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-lg);
border-bottom: 1px solid var(--border-light);
position: sticky;
top: 0;
background: white;
z-index: 10;
}
.btn-close {
width: 32px;
height: 32px;
background: var(--bg-light);
border-radius: 50%;
font-size: var(--font-size-xl);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.btn-close:hover {
background: var(--border-light);
}
.sidebar-content {
padding: var(--spacing-lg);
}
#task-detail-title {
font-size: var(--font-size-xl);
font-weight: 600;
margin-bottom: var(--spacing-md);
}
.task-actions {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.task-actions button {
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
transition: opacity 0.2s;
}
.task-actions button:first-child {
background: var(--primary);
color: white;
}
.task-actions button.danger {
background: var(--danger);
color: white;
}
.task-actions button:hover {
opacity: 0.9;
}
.detail-section {
margin-bottom: var(--spacing-lg);
}
.detail-section label {
display: block;
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.detail-section select, .detail-section input {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
}
.markdown-content {
background: var(--bg-light);
padding: var(--spacing-md);
border-radius: var(--radius-md);
line-height: 1.8;
font-size: var(--font-size-sm);
}
.markdown-content h2 {
font-size: var(--font-size-lg);
margin-top: var(--spacing-md);
margin-bottom: var(--spacing-sm);
}
.markdown-content h3 {
font-size: var(--font-size-md);
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.markdown-content ul, .markdown-content ol {
margin-left: var(--spacing-lg);
}
.markdown-content code {
background: rgba(0, 0, 0, 0.05);
padding: 2px 6px;
border-radius: var(--radius-sm);
font-family: var(--font-family-mono);
font-size: var(--font-size-xs);
}
#attachment-list, #subtask-list {
list-style: none;
margin-bottom: var(--spacing-sm);
}
#attachment-list li, #subtask-list li {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-sm);
background: var(--bg-light);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-xs);
font-size: var(--font-size-sm);
}
#subtask-list li {
cursor: pointer;
transition: background 0.2s;
}
#subtask-list li:hover {
background: var(--border-lighter);
}
#subtask-list li.completed {
color: var(--text-secondary);
text-decoration: line-through;
}
.detail-section button {
width: 100%;
padding: var(--spacing-sm);
background: var(--bg-light);
border-radius: var(--radius-md);
font-size: var(--font-size-sm);
transition: background 0.2s;
}
.detail-section button:hover {
background: var(--border-light);
}
/* ==================== 遮罩层样式 ==================== */
#overlay {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
z-index: 999;
}
#overlay.active {
opacity: 1;
pointer-events: auto;
}
/* ==================== Modal 样式 ==================== */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: var(--radius-lg);
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-lg);
border-bottom: 1px solid var(--border-light);
}
.modal-header h2 {
font-size: var(--font-size-xl);
}
.modal-body {
padding: var(--spacing-lg);
}
/* ==================== Toast 提示样式 ==================== */
.toast {
position: fixed;
top: 80px;
right: var(--spacing-lg);
background: white;
padding: var(--spacing-md) var(--spacing-lg);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-sm);
z-index: 3000;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast.success { border-left: 4px solid var(--success); }
.toast.error { border-left: 4px solid var(--danger); }
.toast.warning { border-left: 4px solid var(--warning); }
.toast.info { border-left: 4px solid var(--info); }
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
#sidebar {
position: fixed;
left: 0;
top: 60px;
height: calc(100vh - 60px);
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 200;
box-shadow: 4px 0 10px rgba(0,0,0,0.3);
}
#sidebar.open {
transform: translateX(0);
}
#sidebar.open::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: -1;
}
.mobile-menu-btn {
display: flex;
align-items: center;
justify-content: center;
}
.header-center {
display: none;
}
.header-left h1 {
font-size: 16px;
}
#task-detail-sidebar {
width: 100%;
}
.kanban-column {
min-width: 240px;
}
}
/* ==================== Dashboard 样式 ==================== */
/* ==================== 智能体管理页样式 ==================== */
.agents-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-lg);
}
.agents-header h2 {
font-size: var(--font-size-xxl);
font-weight: 600;
}
.filter-tabs-agents {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
}
.filter-tabs-agents button {
padding: var(--spacing-sm) var(--spacing-lg);
background: white;
border: 1px solid var(--border-base);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s;
font-size: var(--font-size-sm);
}
.filter-tabs-agents button:hover {
border-color: var(--primary);
color: var(--primary);
}
.filter-tabs-agents button.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.agents-table-container {
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.agents-table {
width: 100%;
border-collapse: collapse;
}
.agents-table thead {
background: var(--bg-dark);
}
.agents-table th {
padding: var(--spacing-md) var(--spacing-lg);
text-align: left;
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.agents-table td {
padding: var(--spacing-md) var(--spacing-lg);
border-top: 1px solid var(--border-lighter);
font-size: var(--font-size-sm);
}
.agents-table tbody tr:hover {
background: var(--bg-light);
}
.agent-status {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: 4px 12px;
border-radius: var(--radius-md);
font-size: var(--font-size-xs);
font-weight: 600;
}
.agent-status.idle {
background: rgba(144, 147, 153, 0.1);
color: var(--info);
}
.agent-status.busy {
background: rgba(103, 194, 58, 0.1);
color: var(--success);
}
.agent-status.offline {
background: rgba(245, 108, 108, 0.1);
color: var(--danger);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* ==================== 规约管理页样式 ==================== */
#content-rules {
display: flex;
flex-direction: column;
height: 100%;
}
.rules-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-lg);
}
.rules-header h2 {
font-size: var(--font-size-xxl);
font-weight: 600;
}
.rules-tabs {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--border-light);
}
.rules-tab {
padding: var(--spacing-md) var(--spacing-lg);
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: var(--font-size-md);
font-weight: 600;
color: var(--text-secondary);
transition: all 0.2s;
margin-bottom: -2px;
}
.rules-tab:hover {
color: var(--primary);
}
.rules-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.rules-tab-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.rules-tab-content.hidden {
display: none;
}
.rules-toolbar {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.btn-add-rule {
padding: var(--spacing-sm) var(--spacing-md);
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: var(--font-size-sm);
font-weight: 600;
transition: opacity 0.2s;
}
.btn-add-rule:hover {
opacity: 0.9;
}
.rules-table-container {
flex: 1;
background: white;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
overflow: auto;
}
.rules-table {
width: 100%;
border-collapse: collapse;
}
.rules-table thead {
background: var(--bg-dark);
position: sticky;
top: 0;
z-index: 10;
}
.rules-table th {
padding: var(--spacing-md) var(--spacing-lg);
text-align: left;
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid var(--border-light);
}
.rules-table td {
padding: var(--spacing-md) var(--spacing-lg);
border-bottom: 1px solid var(--border-lighter);
font-size: var(--font-size-sm);
vertical-align: top;
}
.rules-table tbody tr:hover {
background: var(--bg-light);
}
.rule-type-badge {
display: inline-block;
padding: 4px 12px;
background: rgba(102, 126, 234, 0.1);
color: var(--primary);
border-radius: var(--radius-md);
font-size: var(--font-size-xs);
font-weight: 600;
}
.rule-actions {
display: flex;
gap: var(--spacing-xs);
}
.rule-actions button {
padding: 4px 12px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: var(--font-size-xs);
transition: all 0.2s;
}
.rule-actions .btn-edit {
background: rgba(103, 194, 58, 0.1);
color: var(--success);
}
.rule-actions .btn-edit:hover {
background: var(--success);
color: white;
}
.rule-actions .btn-delete {
background: rgba(245, 108, 108, 0.1);
color: var(--danger);
}
.rule-actions .btn-delete:hover {
background: var(--danger);
color: white;
}
</style>
</head>
<body>
<div id="app">
<!-- Header -->
<header id="header">
<div class="header-left">
<button id="btn-mobile-menu" class="mobile-menu-btn"></button>
<h1>🤖 任务管理系统 v3.0</h1>
</div>
<div class="header-center">
<div class="stat-mini">📁 项目: <span id="stat-projects">0</span></div>
<div class="stat-mini">📋 任务: <span id="stat-tasks">0</span></div>
<div class="stat-mini">🤖 智能体: <span id="stat-agents">0/0</span></div>
</div>
</header>
<!-- App Body -->
<div class="app-body">
<!-- Sidebar -->
<aside id="sidebar">
<div class="sidebar-header">
<button id="btn-collapse"></button>
</div>
<div class="sidebar-section">
<div class="section-title">
📁 项目
<button id="btn-new-project" title="创建新项目">+</button>
</div>
<ul id="project-list">
<!-- 动态渲染项目列表 -->
</ul>
</div>
<!-- 设置菜单 -->
<div class="sidebar-section">
<div class="section-title">⚙️ 设置</div>
<ul id="nav-menu" class="settings-menu">
<!-- 动态渲染导航按钮 -->
</ul>
</div>
<div class="sidebar-section">
<div class="section-title">🤖 智能体</div>
<div class="agent-summary">
<div>⚪ 空闲: <span id="agents-idle">0</span></div>
<div>🟢 工作: <span id="agents-busy">0</span></div>
</div>
</div>
</aside>
<!-- Main Content -->
<main id="main-content">
<!-- Loading -->
<div id="content-loading" class="hidden">
<div class="spinner"></div>
</div>
<!-- Empty State -->
<div id="content-empty" class="empty-state">
<div style="font-size: 64px;">📁</div>
<p>请选择一个项目</p>
</div>
<!-- Kanban Board -->
<div id="content-task-board" class="hidden">
<div id="kanban-board">
<div class="kanban-header">
<button id="btn-new-task">+ 新建任务</button>
<input type="text" id="task-search" placeholder="🔍 搜索任务...">
<select id="task-filter-type">
<option value="">所有类型</option>
<option value="requirement">需求</option>
<option value="design">设计</option>
<option value="development">开发</option>
<option value="testing">测试</option>
<option value="deployment">部署</option>
</select>
<select id="task-filter-priority">
<option value="">所有优先级</option>
<option value="high"></option>
<option value="normal">普通</option>
<option value="low"></option>
</select>
</div>
<div class="kanban-columns">
<div class="kanban-column" data-status="initial">
<h3>初始 <span class="count" id="count-initial">0</span></h3>
<div class="task-list" id="tasks-initial"></div>
</div>
<div class="kanban-column" data-status="in_progress">
<h3>进行中 <span class="count" id="count-in_progress">0</span></h3>
<div class="task-list" id="tasks-in_progress"></div>
</div>
<div class="kanban-column" data-status="testing_pending">
<h3>待测试 <span class="count" id="count-testing_pending">0</span></h3>
<div class="task-list" id="tasks-testing_pending"></div>
</div>
<div class="kanban-column" data-status="testing">
<h3>测试中 <span class="count" id="count-testing">0</span></h3>
<div class="task-list" id="tasks-testing"></div>
</div>
<div class="kanban-column" data-status="completed">
<h3>完成 <span class="count" id="count-completed">0</span></h3>
<div class="task-list" id="tasks-completed"></div>
</div>
</div>
</div>
</div>
<!-- Agents Management -->
<div id="content-agents" class="hidden">
<div class="agents-header">
<h2>🤖 智能体管理</h2>
<button id="btn-register-agent" style="padding: 8px 16px; background: #667eea; color: white; border-radius: 8px;">注册新智能体</button>
</div>
<div class="filter-tabs-agents">
<button class="active" data-filter="all">全部</button>
<button data-filter="idle">空闲</button>
<button data-filter="busy">工作中</button>
</div>
<div class="agents-table-container">
<table class="agents-table">
<thead>
<tr>
<th>名称</th>
<th>状态</th>
<th>当前任务</th>
<th>完成数</th>
<th>最后心跳</th>
</tr>
</thead>
<tbody id="agents-table-body">
<tr><td colspan="5" style="text-align: center; padding: 32px; color: #909399;">暂无数据</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Rules Management -->
<div id="content-rules" class="hidden">
<div class="rules-header">
<h2>📜 协作规约</h2>
</div>
<div class="rules-tabs">
<button class="rules-tab active" data-tab="global">全局规约</button>
<button class="rules-tab" data-tab="project">项目规约</button>
</div>
<div id="rules-tab-global" class="rules-tab-content">
<div class="rules-toolbar">
<button id="btn-add-global-rule" class="btn-add-rule">+ 添加全局规约</button>
</div>
<div class="rules-table-container">
<table class="rules-table">
<thead>
<tr>
<th width="15%">规约编号</th>
<th width="15%">规约类型</th>
<th width="55%">内容描述</th>
<th width="15%">操作</th>
</tr>
</thead>
<tbody id="global-rules-body">
<tr><td colspan="4" style="text-align: center; padding: 32px; color: #909399;">暂无规约</td></tr>
</tbody>
</table>
</div>
</div>
<div id="rules-tab-project" class="rules-tab-content hidden">
<div class="rules-toolbar">
<button id="btn-add-project-rule" class="btn-add-rule">+ 添加项目规约</button>
</div>
<div class="rules-table-container">
<table class="rules-table">
<thead>
<tr>
<th width="12%">项目编号</th>
<th width="13%">规约编号</th>
<th width="13%">规约类型</th>
<th width="47%">内容描述</th>
<th width="15%">操作</th>
</tr>
</thead>
<tbody id="project-rules-body">
<tr><td colspan="5" style="text-align: center; padding: 32px; color: #909399;">暂无规约</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</main>
</div>
<!-- Task Detail Sidebar -->
<div id="task-detail-sidebar">
<div class="sidebar-header-detail">
<h3>任务详情</h3>
<button class="btn-close" id="btn-close-detail">&times;</button>
</div>
<div class="sidebar-content">
<h2 id="task-detail-title">加载中...</h2>
<div class="task-actions">
<button id="btn-edit-task">编辑</button>
<button id="btn-delete-task" class="danger">删除</button>
</div>
<div class="detail-section">
<label>状态</label>
<select id="task-status-select">
<option value="initial">初始</option>
<option value="in_progress">进行中</option>
<option value="testing_pending">待测试</option>
<option value="testing">测试中</option>
<option value="completed">完成</option>
</select>
</div>
<div class="detail-section">
<label>类型</label>
<select id="task-type-select">
<option value="requirement">需求</option>
<option value="design">设计</option>
<option value="development">开发</option>
<option value="testing">测试</option>
<option value="deployment">部署</option>
</select>
</div>
<div class="detail-section">
<label>优先级</label>
<select id="task-priority-select">
<option value="low"></option>
<option value="normal">普通</option>
<option value="high"></option>
</select>
</div>
<div class="detail-section">
<label>执行者</label>
<select id="task-assignee-select">
<option value="">未分配</option>
<!-- 动态加载智能体列表 -->
</select>
</div>
<div class="detail-section">
<label>📝 描述</label>
<div id="task-description" class="markdown-content">
<!-- Markdown 渲染 -->
</div>
</div>
<div class="detail-section">
<label>📎 附件 (<span id="attachment-count">0</span>)</label>
<ul id="attachment-list"></ul>
<input type="file" id="file-upload" hidden>
<button id="btn-upload">上传附件</button>
</div>
<div class="detail-section">
<label>🔀 子任务 (<span id="subtask-progress">0/0</span>)</label>
<ul id="subtask-list"></ul>
<button id="btn-add-subtask">+ 添加子任务</button>
</div>
</div>
</div>
<!-- Overlay -->
<div id="overlay"></div>
<!-- Modal -->
<div id="global-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">标题</h2>
<button class="btn-close" onclick="app.closeModal()">&times;</button>
</div>
<div class="modal-body" id="modal-body">
<!-- 动态内容 -->
</div>
</div>
</div>
</div>
<script>
// ==================== 核心应用类 ====================
class App {
constructor() {
this.api = new API();
this.state = new AppState();
this.currentTaskId = null;
this.currentProjectId = null;
this.init();
}
async init() {
// 默认显示 Dashboard
setTimeout(() => this.showDashboard(), 100);
// 初始化事件监听
this.bindEvents();
// 加载初始数据
await this.loadProjects();
await this.loadAgents();
await this.loadStats();
// 绑定子任务按钮
this.setupSubtaskButton();
// 启动实时更新
this.startRealtimeUpdate();
}
bindEvents() {
// 移动端菜单按钮
const mobileMenuBtn = document.getElementById('btn-mobile-menu');
console.log('Mobile menu button found:', !!mobileMenuBtn);
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', (e) => {
console.log('Mobile menu clicked!');
e.preventDefault();
e.stopPropagation();
this.toggleMobileSidebar();
});
// 添加触摸事件支持
mobileMenuBtn.addEventListener('touchstart', (e) => {
console.log('Mobile menu touched!');
e.preventDefault();
this.toggleMobileSidebar();
}, { passive: false });
}
// Sidebar 按钮
document.getElementById('btn-collapse').addEventListener('click', () => this.toggleSidebar());
document.getElementById('btn-new-project').addEventListener('click', () => this.showCreateProjectForm());
// Kanban 按钮
document.getElementById('btn-new-task').addEventListener('click', () => this.showCreateTaskForm());
document.getElementById('task-search').addEventListener('input', (e) => this.handleSearch(e.target.value));
document.getElementById('task-filter-type').addEventListener('change', () => this.applyFilters());
document.getElementById('task-filter-priority').addEventListener('change', () => this.applyFilters());
// Task Detail Sidebar
document.getElementById('btn-close-detail').addEventListener('click', () => this.closeTaskDetail());
document.getElementById('overlay').addEventListener('click', () => this.closeTaskDetail());
document.getElementById('task-status-select').addEventListener('change', (e) => this.updateTaskField('status', e.target.value));
document.getElementById('task-type-select').addEventListener('change', (e) => this.updateTaskField('type', e.target.value));
document.getElementById('task-priority-select').addEventListener('change', (e) => this.updateTaskField('priority', e.target.value));
document.getElementById('task-assignee-select').addEventListener('change', (e) => this.updateTaskField('assigned_to', e.target.value));
document.getElementById('btn-delete-task').addEventListener('click', () => this.deleteTask());
document.getElementById('btn-upload').addEventListener('click', () => document.getElementById('file-upload').click());
document.getElementById('file-upload').addEventListener('change', (e) => this.uploadAttachment(e.target.files[0]));
}
toggleSidebar() {
document.getElementById('sidebar').classList.toggle('collapsed');
}
toggleMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const isOpen = sidebar.classList.contains('open');
console.log('Toggle mobile sidebar, current state:', isOpen ? 'open' : 'closed');
if (isOpen) {
sidebar.classList.remove('open');
} else {
sidebar.classList.add('open');
// 点击外部关闭侧边栏
setTimeout(() => {
const closeOnClick = (e) => {
if (!sidebar.contains(e.target) && e.target.id !== 'btn-mobile-menu') {
sidebar.classList.remove('open');
document.removeEventListener('click', closeOnClick);
document.removeEventListener('touchstart', closeOnClick);
}
};
document.addEventListener('click', closeOnClick);
document.addEventListener('touchstart', closeOnClick);
}, 100);
}
}
// ==================== 项目管理 ====================
async loadProjects() {
try {
const projects = await this.api.get('/projects');
this.state.projects = projects;
this.renderProjects();
} catch (error) {
this.showToast('加载项目失败: ' + error.message, 'error');
}
}
renderProjects() {
const sidebar = document.getElementById('sidebar');
const isCollapsed = sidebar.classList.contains('collapsed');
// 渲染导航菜单(只保留智能体管理和规约)
const navMenu = document.getElementById('nav-menu');
navMenu.innerHTML = '';
const agentsBtn = document.createElement('li');
agentsBtn.className = 'nav-item';
agentsBtn.innerHTML = isCollapsed ? '🤖' : '智能体管理';
agentsBtn.title = '智能体管理';
const isAgents = !document.getElementById('content-agents').classList.contains('hidden');
if (isAgents) {
agentsBtn.style.background = '#667eea';
agentsBtn.style.color = 'white';
}
agentsBtn.addEventListener('click', () => this.showAgentsPage());
const rulesBtn = document.createElement('li');
rulesBtn.className = 'nav-item';
rulesBtn.innerHTML = isCollapsed ? '📜' : '协作规约';
rulesBtn.title = '协作规约';
const isRules = !document.getElementById('content-rules').classList.contains('hidden');
if (isRules) {
rulesBtn.style.background = '#667eea';
rulesBtn.style.color = 'white';
}
rulesBtn.addEventListener('click', () => this.showRulesPage());
navMenu.appendChild(agentsBtn);
navMenu.appendChild(rulesBtn);
// 渲染项目列表
const list = document.getElementById('project-list');
list.innerHTML = '';
if (this.state.projects.length === 0) {
const emptyMsg = document.createElement('li');
emptyMsg.style.cssText = 'padding: 8px; color: #909399; font-size: 12px;';
emptyMsg.textContent = isCollapsed ? '' : '暂无项目';
list.appendChild(emptyMsg);
return;
}
this.state.projects.forEach(project => {
const li = document.createElement('li');
li.className = 'project-item';
if (project.id === this.currentProjectId) {
li.classList.add('active');
}
// 创建项目名称容器
const nameSpan = document.createElement('span');
nameSpan.textContent = project.name;
nameSpan.style.flex = '1';
nameSpan.addEventListener('click', () => this.selectProject(project.id));
// 创建删除按钮
const deleteBtn = document.createElement('button');
deleteBtn.innerHTML = '×';
deleteBtn.className = 'project-delete-btn';
deleteBtn.title = '删除项目';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.deleteProject(project.id, project.name);
});
li.style.display = 'flex';
li.style.alignItems = 'center';
li.style.gap = '8px';
li.appendChild(nameSpan);
li.appendChild(deleteBtn);
list.appendChild(li);
});
}
async deleteProject(projectId, projectName) {
if (!confirm(`确定要删除项目"${projectName}"吗?\n\n删除后该项目下的所有任务也将被删除,此操作不可恢复!`)) {
return;
}
try {
await this.api.delete(`/projects/${projectId}`);
this.showToast('项目已删除', 'success');
// 如果删除的是当前选中的项目,回到 Dashboard
if (this.currentProjectId === projectId) {
this.currentProjectId = null;
await this.showDashboard();
}
// 重新加载项目列表和统计
await this.loadProjects();
await this.loadStats();
} catch (error) {
this.showToast('删除失败: ' + error.message, 'error');
}
}
// ==================== Dashboard 功能 ====================
// ==================== 智能体管理功能 ====================
showAgentsPage() {
// 隐藏其他内容
document.getElementById('content-empty').classList.add('hidden');
document.getElementById('content-task-board').classList.add('hidden');
document.getElementById('content-agents').classList.remove('hidden');
document.getElementById('content-rules').classList.add('hidden');
// 清除项目选中
this.currentProjectId = null;
this.renderProjects();
// 渲染智能体表格
this.renderAgentsTable();
// 绑定筛选按钮
document.querySelectorAll('.filter-tabs-agents button').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.filter-tabs-agents button').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
this.filterAgents(e.target.dataset.filter);
});
});
// 绑定注册智能体按钮
const registerBtn = document.getElementById('btn-register-agent');
if (registerBtn) {
// 移除旧的事件监听器
const newBtn = registerBtn.cloneNode(true);
registerBtn.parentNode.replaceChild(newBtn, registerBtn);
newBtn.addEventListener('click', () => this.showRegisterAgentForm());
}
}
renderAgentsTable(filter = 'all') {
const tbody = document.getElementById('agents-table-body');
tbody.innerHTML = '';
let agents = this.state.agents;
if (filter === 'idle') agents = agents.filter(a => a.status === 'idle');
if (filter === 'busy') agents = agents.filter(a => a.status === 'busy');
if (agents.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 32px; color: #909399;">暂无数据</td></tr>';
return;
}
agents.forEach(agent => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td><strong>${this.escapeHtml(agent.name)}</strong></td>
<td>
<span class="agent-status ${agent.status}">
<span class="status-dot"></span>
${agent.status === 'idle' ? '空闲' : agent.status === 'busy' ? '工作中' : '离线'}
</span>
</td>
<td>${agent.current_task || '-'}</td>
<td>${agent.completed_tasks || 0}</td>
<td>${this.formatDate(agent.last_heartbeat)}</td>
`;
tbody.appendChild(tr);
});
}
filterAgents(filter) {
this.renderAgentsTable(filter);
}
// ==================== 规约管理功能 ====================
async showRulesPage() {
// 隐藏其他内容
document.getElementById('content-empty').classList.add('hidden');
document.getElementById('content-task-board').classList.add('hidden');
document.getElementById('content-agents').classList.add('hidden');
document.getElementById('content-rules').classList.remove('hidden');
// 清除项目选中
this.currentProjectId = null;
this.renderProjects();
// 加载规约数据
await this.loadRules();
// 绑定 Tab 切换
document.querySelectorAll('.rules-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
document.querySelectorAll('.rules-tab').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
const targetTab = e.target.dataset.tab;
document.querySelectorAll('.rules-tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`rules-tab-${targetTab}`).classList.remove('hidden');
});
});
// 绑定添加按钮
const addGlobalBtn = document.getElementById('btn-add-global-rule');
if (addGlobalBtn) {
const newBtn = addGlobalBtn.cloneNode(true);
addGlobalBtn.parentNode.replaceChild(newBtn, addGlobalBtn);
newBtn.addEventListener('click', () => this.showAddRuleForm(null));
}
const addProjectBtn = document.getElementById('btn-add-project-rule');
if (addProjectBtn) {
const newBtn = addProjectBtn.cloneNode(true);
addProjectBtn.parentNode.replaceChild(newBtn, addProjectBtn);
newBtn.addEventListener('click', () => this.showAddRuleForm(this.currentProjectId));
}
}
async loadRules() {
try {
const globalRules = await this.api.get('/rules/global');
this.renderRulesTable('global', globalRules);
// 暂时不加载项目规约,因为没有选中项目
this.renderRulesTable('project', []);
} catch (error) {
console.error('加载规约失败:', error);
}
}
renderRulesTable(type, rules) {
const tbody = document.getElementById(`${type}-rules-body`);
tbody.innerHTML = '';
if (!rules || rules.length === 0) {
const colspan = type === 'global' ? 4 : 5;
tbody.innerHTML = `<tr><td colspan="${colspan}" style="text-align: center; padding: 32px; color: #909399;">暂无规约</td></tr>`;
return;
}
rules.forEach(rule => {
const tr = document.createElement('tr');
tr.innerHTML = `
${type === 'project' ? `<td>${rule.project_id || '-'}</td>` : ''}
<td><strong>${this.escapeHtml(rule.rule_no)}</strong></td>
<td><span class="rule-type-badge">${this.escapeHtml(rule.rule_type)}</span></td>
<td>${this.escapeHtml(rule.description)}</td>
<td>
<div class="rule-actions">
<button class="btn-edit" onclick="app.editRule(${rule.id})">编辑</button>
<button class="btn-delete" onclick="app.deleteRule(${rule.id}, '${type}')">删除</button>
</div>
</td>
`;
tbody.appendChild(tr);
});
}
async deleteRule(ruleId, type) {
if (!confirm('确定要删除这条规约吗?')) return;
try {
await this.api.delete(`/rules/${ruleId}`);
this.showToast('规约已删除', 'success');
await this.loadRules();
} catch (error) {
this.showToast('删除失败: ' + error.message, 'error');
}
}
showAddRuleForm(projectId) {
const formHTML = `
<form id="form-add-rule" style="display: flex; flex-direction: column; gap: 16px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">规约编号</label>
<input type="text" name="rule_no" placeholder="例如: R001" required
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">规约类型</label>
<select name="rule_type" required style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
<option value="">请选择</option>
<option value="代码规范">代码规范</option>
<option value="提交规范">提交规范</option>
<option value="测试规范">测试规范</option>
<option value="文档规范">文档规范</option>
<option value="安全规范">安全规范</option>
<option value="其他">其他</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">内容描述</label>
<textarea name="description" placeholder="输入规约详细描述" rows="4" required
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;"></textarea>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button type="button" onclick="app.closeModal()"
style="padding: 8px 16px; background: #f0f2f5; border-radius: 8px; border: none; cursor: pointer;">取消</button>
<button type="submit"
style="padding: 8px 16px; background: #667eea; color: white; border-radius: 8px; border: none; cursor: pointer;">添加</button>
</div>
</form>
`;
this.showModal(projectId ? '添加项目规约' : '添加全局规约', formHTML);
document.getElementById('form-add-rule').addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const rule = {
project_id: projectId,
rule_no: data.get('rule_no'),
rule_type: data.get('rule_type'),
description: data.get('description')
};
try {
await this.api.post('/rules', rule);
this.showToast('规约添加成功', 'success');
this.closeModal();
await this.loadRules();
} catch (error) {
this.showToast('添加失败: ' + error.message, 'error');
}
});
}
// ==================== 智能体注册表单 ====================
showRegisterAgentForm() {
const formHTML = `
<form id="form-register-agent" style="display: flex; flex-direction: column; gap: 16px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">智能体名称</label>
<input type="text" name="name" placeholder="例如: Agent-001" required
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">类型</label>
<select name="type" style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
<option value="backend">后端开发</option>
<option value="frontend">前端开发</option>
<option value="testing">测试</option>
<option value="devops">运维</option>
<option value="design">设计</option>
<option value="general">通用</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">能力标签 (用逗号分隔)</label>
<input type="text" name="capabilities" placeholder="例如: python, database, api"
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
<small style="color: #909399; font-size: 12px;">多个能力请用逗号分隔,如: python, react, docker</small>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button type="button" onclick="app.closeModal()"
style="padding: 8px 16px; background: #f0f2f5; border-radius: 8px;">取消</button>
<button type="submit"
style="padding: 8px 16px; background: #667eea; color: white; border-radius: 8px;">注册</button>
</div>
</form>
`;
this.showModal('注册新智能体', formHTML);
document.getElementById('form-register-agent').addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const capabilitiesStr = data.get('capabilities');
const capabilities = capabilitiesStr ? capabilitiesStr.split(',').map(c => c.trim()).filter(c => c) : [];
const agent = {
name: data.get('name'),
type: data.get('type'),
capabilities: capabilities
};
try {
await this.api.post('/agents', agent);
this.showToast('智能体注册成功', 'success');
this.closeModal();
await this.loadAgents();
this.renderAgentsTable();
} catch (error) {
this.showToast('注册失败: ' + error.message, 'error');
}
});
}
// ==================== 工具方法 ====================
formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 60) return `${minutes}分钟前`;
if (hours < 24) return `${hours}小时前`;
if (days < 7) return `${days}天前`;
return date.toLocaleDateString('zh-CN');
}
async selectProject(projectId) {
this.currentProjectId = projectId;
this.renderProjects();
// 隐藏所有其他内容
document.getElementById('content-empty').classList.add('hidden');
document.getElementById('content-agents').classList.add('hidden');
document.getElementById('content-rules').classList.add('hidden');
// 只显示看板
document.getElementById('content-task-board').classList.remove('hidden');
// 加载任务
await this.loadKanbanBoard();
}
showCreateProjectForm() {
const formHTML = `
<form id="form-create-project" style="display: flex; flex-direction: column; gap: 16px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">项目名称</label>
<input type="text" name="name" placeholder="输入项目名称" required
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">项目描述</label>
<textarea name="description" placeholder="输入项目描述" rows="4"
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;"></textarea>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button type="button" onclick="app.closeModal()"
style="padding: 8px 16px; background: #f0f2f5; border-radius: 8px;">取消</button>
<button type="submit"
style="padding: 8px 16px; background: #667eea; color: white; border-radius: 8px;">创建</button>
</div>
</form>
`;
this.showModal('创建项目', formHTML);
document.getElementById('form-create-project').addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const project = {
name: data.get('name'),
description: data.get('description'),
created_by: 'human'
};
try {
await this.api.post('/projects', project);
this.showToast('项目创建成功', 'success');
this.closeModal();
await this.loadProjects();
await this.loadStats();
} catch (error) {
this.showToast('创建失败: ' + error.message, 'error');
}
});
}
// ==================== 任务看板 ====================
async loadKanbanBoard() {
if (!this.currentProjectId) return;
try {
const tasks = await this.api.get(`/tasks?project_id=${this.currentProjectId}`);
this.state.tasks = tasks;
this.renderKanbanBoard();
this.initDragDrop();
} catch (error) {
this.showToast('加载任务失败: ' + error.message, 'error');
}
}
renderKanbanBoard() {
const statuses = ['initial', 'in_progress', 'testing_pending', 'testing', 'completed'];
// 清空所有列
statuses.forEach(status => {
document.getElementById(`tasks-${status}`).innerHTML = '';
document.getElementById(`count-${status}`).textContent = '0';
});
// 应用过滤器
const filtered = this.getFilteredTasks();
// 按状态分组
const grouped = {};
statuses.forEach(s => grouped[s] = []);
filtered.forEach(task => {
if (grouped[task.status]) {
grouped[task.status].push(task);
}
});
// 渲染每列
statuses.forEach(status => {
const container = document.getElementById(`tasks-${status}`);
const tasks = grouped[status];
document.getElementById(`count-${status}`).textContent = tasks.length;
tasks.forEach(task => {
container.appendChild(this.createTaskCard(task));
});
});
}
createTaskCard(task) {
const card = document.createElement('div');
card.className = 'task-card';
card.dataset.taskId = task.id;
const typeClass = `type-${task.type}`;
const priorityClass = `priority-${task.priority}`;
const typeNames = {
requirement: '需求',
design: '设计',
development: '开发',
testing: '测试',
deployment: '部署'
};
const priorityNames = {
low: '低',
normal: '普通',
high: '高'
};
card.innerHTML = `
<div class="task-title">${this.escapeHtml(task.title)}</div>
<div class="task-meta">
<span class="badge ${typeClass}">${typeNames[task.type] || task.type}</span>
<span class="badge ${priorityClass}">${priorityNames[task.priority] || task.priority}</span>
</div>
<div class="task-footer">
<span class="assignee">${task.assigned_to ? '🤖 ' + task.assigned_to : '未分配'}</span>
<div class="task-icons">
${task.subtask_count > 0 ? `<span>🔗 ${task.subtask_count}</span>` : ''}
${task.attachment_count > 0 ? `<span>📎 ${task.attachment_count}</span>` : ''}
</div>
</div>
`;
card.addEventListener('click', () => this.showTaskDetail(task.id));
return card;
}
getFilteredTasks() {
let filtered = this.state.tasks;
// 搜索过滤
const searchTerm = document.getElementById('task-search').value.toLowerCase();
if (searchTerm) {
filtered = filtered.filter(task =>
task.title.toLowerCase().includes(searchTerm) ||
(task.description && task.description.toLowerCase().includes(searchTerm))
);
}
// 类型过滤
const typeFilter = document.getElementById('task-filter-type').value;
if (typeFilter) {
filtered = filtered.filter(task => task.type === typeFilter);
}
// 优先级过滤
const priorityFilter = document.getElementById('task-filter-priority').value;
if (priorityFilter) {
filtered = filtered.filter(task => task.priority === priorityFilter);
}
return filtered;
}
handleSearch(term) {
// 防抖处理
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.renderKanbanBoard();
}, 300);
}
applyFilters() {
this.renderKanbanBoard();
}
initDragDrop() {
const statuses = ['initial', 'in_progress', 'testing_pending', 'testing', 'completed'];
statuses.forEach(status => {
const container = document.getElementById(`tasks-${status}`);
new Sortable(container, {
group: 'tasks',
animation: 150,
ghostClass: 'task-ghost',
chosenClass: 'task-chosen',
dragClass: 'dragging',
onEnd: async (evt) => {
const taskId = evt.item.dataset.taskId;
const newStatus = evt.to.parentElement.dataset.status;
const oldStatus = evt.from.parentElement.dataset.status;
if (oldStatus !== newStatus) {
await this.updateTaskStatus(taskId, newStatus);
}
}
});
});
}
async updateTaskStatus(taskId, newStatus) {
try {
await this.api.put(`/tasks/${taskId}`, { status: newStatus });
this.showToast('任务状态已更新', 'success');
// 更新本地状态
const task = this.state.tasks.find(t => t.id === taskId);
if (task) {
task.status = newStatus;
}
// 更新计数
this.renderKanbanBoard();
} catch (error) {
this.showToast('更新失败: ' + error.message, 'error');
// 刷新看板恢复原状
await this.loadKanbanBoard();
}
}
showCreateTaskForm() {
if (!this.currentProjectId) {
this.showToast('请先选择一个项目', 'warning');
return;
}
const formHTML = `
<form id="form-create-task" style="display: flex; flex-direction: column; gap: 16px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">任务标题</label>
<input type="text" name="title" placeholder="输入任务标题" required
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">任务描述 (支持 Markdown)</label>
<textarea name="description" placeholder="输入任务描述" rows="6"
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;"></textarea>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">类型</label>
<select name="type" style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
<option value="requirement">需求</option>
<option value="design">设计</option>
<option value="development" selected>开发</option>
<option value="testing">测试</option>
<option value="deployment">部署</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">优先级</label>
<select name="priority" style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
<option value="low">低</option>
<option value="normal" selected>普通</option>
<option value="high">高</option>
</select>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">父任务 (可选)</label>
<select name="parent_id" id="parent-task-select" style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
<option value="">无 (顶级任务)</option>
<!-- 动态加载 -->
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">执行者 (可选)</label>
<select name="assigned_to" id="create-assignee-select" style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
<option value="">未分配</option>
<!-- 动态加载 -->
</select>
</div>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button type="button" onclick="app.closeModal()"
style="padding: 8px 16px; background: #f0f2f5; border-radius: 8px;">取消</button>
<button type="submit"
style="padding: 8px 16px; background: #667eea; color: white; border-radius: 8px;">创建</button>
</div>
</form>
`;
this.showModal('创建任务', formHTML);
// 填充父任务选项
const parentSelect = document.getElementById('parent-task-select');
this.state.tasks.filter(t => !t.parent_id).forEach(task => {
const option = document.createElement('option');
option.value = task.id;
option.textContent = task.title;
parentSelect.appendChild(option);
});
// 填充执行者选项
const assigneeSelect = document.getElementById('create-assignee-select');
this.state.agents.forEach(agent => {
const option = document.createElement('option');
option.value = agent.name;
option.textContent = `${agent.name} (${agent.status === 'idle' ? '空闲' : '工作中'})`;
assigneeSelect.appendChild(option);
});
document.getElementById('form-create-task').addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const task = {
project_id: this.currentProjectId,
title: data.get('title'),
description: data.get('description'),
type: data.get('type'),
priority: data.get('priority'),
status: 'initial',
created_by: 'human',
parent_id: data.get('parent_id') || null,
assigned_to: data.get('assigned_to') || null
};
try {
await this.api.post('/tasks', task);
this.showToast('任务创建成功', 'success');
this.closeModal();
await this.loadKanbanBoard();
await this.loadStats();
} catch (error) {
this.showToast('创建失败: ' + error.message, 'error');
}
});
}
// ==================== 任务详情 ====================
async showTaskDetail(taskId) {
this.currentTaskId = taskId;
try {
const task = await this.api.get(`/tasks/${taskId}`);
this.renderTaskDetail(task);
// 显示侧边栏
document.getElementById('task-detail-sidebar').classList.add('active');
document.getElementById('overlay').classList.add('active');
} catch (error) {
this.showToast('加载任务详情失败: ' + error.message, 'error');
}
}
renderTaskDetail(task) {
document.getElementById('task-detail-title').textContent = task.title;
document.getElementById('task-status-select').value = task.status;
document.getElementById('task-type-select').value = task.type;
document.getElementById('task-priority-select').value = task.priority;
document.getElementById('task-assignee-select').value = task.assigned_to || '';
// 渲染描述 (Markdown)
const descriptionEl = document.getElementById('task-description');
if (task.description) {
descriptionEl.innerHTML = marked.parse(task.description);
} else {
descriptionEl.innerHTML = '<p style="color: #909399;">暂无描述</p>';
}
// 渲染附件
this.renderAttachments(task.attachments || []);
// 渲染子任务
this.renderSubtasks(task.subtasks || []);
}
renderAttachments(attachments) {
const list = document.getElementById('attachment-list');
const count = document.getElementById('attachment-count');
list.innerHTML = '';
count.textContent = attachments.length;
attachments.forEach(att => {
const li = document.createElement('li');
li.innerHTML = `
<span>${this.escapeHtml(att.filename)}</span>
<div>
<a href="/api/attachments/${att.id}/download" download style="margin-right: 8px; color: #667eea;">下载</a>
<span style="cursor: pointer; color: #f56c6c;" onclick="app.deleteAttachment('${att.id}')">✕</span>
</div>
`;
list.appendChild(li);
});
}
renderSubtasks(subtasks) {
const list = document.getElementById('subtask-list');
const progress = document.getElementById('subtask-progress');
list.innerHTML = '';
const completed = subtasks.filter(st => st.status === 'completed').length;
progress.textContent = `${completed}/${subtasks.length}`;
subtasks.forEach(st => {
const li = document.createElement('li');
li.className = st.status === 'completed' ? 'completed' : '';
li.innerHTML = `
<span>${st.status === 'completed' ? '✓' : '☐'} ${this.escapeHtml(st.title)}</span>
`;
li.addEventListener('click', () => this.toggleSubtask(st.id, st.status));
list.appendChild(li);
});
}
// ==================== 子任务管理 ====================
setupSubtaskButton() {
const btn = document.getElementById('btn-add-subtask');
btn.addEventListener('click', () => this.showAddSubtaskForm());
}
showAddSubtaskForm() {
const formHTML = `
<form id="form-add-subtask" style="display: flex; flex-direction: column; gap: 12px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">子任务标题</label>
<input type="text" name="title" placeholder="输入子任务标题" required
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button type="button" onclick="app.closeModal()"
style="padding: 8px 16px; background: #f0f2f5; border-radius: 8px;">取消</button>
<button type="submit"
style="padding: 8px 16px; background: #667eea; color: white; border-radius: 8px;">添加</button>
</div>
</form>
`;
this.showModal('添加子任务', formHTML);
document.getElementById('form-add-subtask').addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const subtask = {
project_id: this.currentProjectId,
parent_id: this.currentTaskId,
title: data.get('title'),
description: '',
type: 'development',
priority: 'normal',
status: 'initial',
created_by: 'human'
};
try {
await this.api.post('/tasks', subtask);
this.showToast('子任务添加成功', 'success');
this.closeModal();
await this.showTaskDetail(this.currentTaskId);
} catch (error) {
this.showToast('添加失败: ' + error.message, 'error');
}
});
}
async toggleSubtask(subtaskId, currentStatus) {
const newStatus = currentStatus === 'completed' ? 'initial' : 'completed';
try {
await this.api.put(`/tasks/${subtaskId}`, { status: newStatus });
this.showToast('子任务状态已更新', 'success');
await this.showTaskDetail(this.currentTaskId);
} catch (error) {
this.showToast('更新失败: ' + error.message, 'error');
}
}
closeTaskDetail() {
document.getElementById('task-detail-sidebar').classList.remove('active');
document.getElementById('overlay').classList.remove('active');
}
async updateTaskField(field, value) {
if (!this.currentTaskId) return;
try {
await this.api.put(`/tasks/${this.currentTaskId}`, { [field]: value });
this.showToast('任务已更新', 'success');
// 更新本地状态
const task = this.state.tasks.find(t => t.id === this.currentTaskId);
if (task) {
task[field] = value;
}
// 如果更新了状态,刷新看板
if (field === 'status') {
this.renderKanbanBoard();
}
} catch (error) {
this.showToast('更新失败: ' + error.message, 'error');
}
}
async deleteTask() {
if (!this.currentTaskId) return;
if (!confirm('确定要删除这个任务吗?')) return;
try {
await this.api.delete(`/tasks/${this.currentTaskId}`);
this.showToast('任务已删除', 'success');
this.closeTaskDetail();
await this.loadKanbanBoard();
await this.loadStats();
} catch (error) {
this.showToast('删除失败: ' + error.message, 'error');
}
}
async uploadAttachment(file) {
if (!file || !this.currentTaskId) return;
if (file.size > 10 * 1024 * 1024) {
this.showToast('文件大小不能超过 10MB', 'warning');
return;
}
const formData = new FormData();
formData.append('file', file);
try {
await this.api.post(`/tasks/${this.currentTaskId}/attachments`, formData);
this.showToast('附件上传成功', 'success');
await this.showTaskDetail(this.currentTaskId);
} catch (error) {
this.showToast('上传失败: ' + error.message, 'error');
}
}
async deleteAttachment(attachmentId) {
if (!confirm('确定要删除这个附件吗?')) return;
try {
await this.api.delete(`/attachments/${attachmentId}`);
this.showToast('附件已删除', 'success');
await this.showTaskDetail(this.currentTaskId);
} catch (error) {
this.showToast('删除失败: ' + error.message, 'error');
}
}
// ==================== 智能体管理 ====================
async loadAgents() {
try {
const agents = await this.api.get('/agents');
this.state.agents = agents;
this.updateAgentSummary();
this.updateAssigneeSelect();
} catch (error) {
console.error('加载智能体失败:', error);
}
}
updateAgentSummary() {
const idle = this.state.agents.filter(a => a.status === 'idle').length;
const busy = this.state.agents.filter(a => a.status === 'busy').length;
document.getElementById('agents-idle').textContent = idle;
document.getElementById('agents-busy').textContent = busy;
document.getElementById('stat-agents').textContent = `${busy}/${this.state.agents.length}`;
}
updateAssigneeSelect() {
const select = document.getElementById('task-assignee-select');
select.innerHTML = '<option value="">未分配</option>';
this.state.agents.forEach(agent => {
const option = document.createElement('option');
option.value = agent.name;
option.textContent = `${agent.name} (${agent.status === 'idle' ? '空闲' : '工作中'})`;
select.appendChild(option);
});
}
// ==================== 统计 ====================
async loadStats() {
try {
const stats = await this.api.get('/stats');
document.getElementById('stat-projects').textContent = stats.projects || 0;
document.getElementById('stat-tasks').textContent = stats.tasks || 0;
} catch (error) {
console.error('加载统计失败:', error);
}
}
// ==================== 全局规约 ====================
async showGlobalRules() { await this.showGlobalRulesEnhanced(); }
// ==================== 完善规约浮层 ====================
async showGlobalRulesEnhanced() {
try {
const globalRules = await this.api.get('/rules/global');
let projectRules = [];
if (this.currentProjectId) {
projectRules = await this.api.get(`/rules/project/${this.currentProjectId}`);
}
const renderRulesTable = (rules, isGlobal) => {
if (!rules || rules.length === 0) {
return '<p style="text-align: center; padding: 32px; color: #909399;">暂无规约</p>';
}
return `
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
<thead style="background: #f5f7fa;">
<tr>
${!isGlobal ? '<th style="padding: 12px; text-align: left; border-bottom: 2px solid #e4e7ed;">项目编号</th>' : ''}
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #e4e7ed;">规约编号</th>
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #e4e7ed;">规约类型</th>
<th style="padding: 12px; text-align: left; border-bottom: 2px solid #e4e7ed;">内容描述</th>
</tr>
</thead>
<tbody>
${rules.map(rule => `
<tr style="border-bottom: 1px solid #ebeef5;">
${!isGlobal ? `<td style="padding: 12px;">${this.currentProjectId || '-'}</td>` : ''}
<td style="padding: 12px; font-weight: 600;">${this.escapeHtml(rule.rule_no)}</td>
<td style="padding: 12px;"><span style="padding: 2px 8px; background: #ecf5ff; color: #409eff; border-radius: 4px; font-size: 12px;">${this.escapeHtml(rule.rule_type)}</span></td>
<td style="padding: 12px;">${this.escapeHtml(rule.description)}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
};
const content = `
<div style="margin-bottom: 16px;">
<div class="modal-tabs" style="display: flex; gap: 8px; border-bottom: 2px solid #e4e7ed; margin-bottom: 16px;">
<button class="modal-tab active" data-tab="global" style="padding: 8px 16px; background: none; border: none; border-bottom: 2px solid #667eea; font-weight: 600; color: #667eea; cursor: pointer; margin-bottom: -2px;">全局规约 (${globalRules.length})</button>
${this.currentProjectId ? `<button class="modal-tab" data-tab="project" style="padding: 8px 16px; background: none; border: none; font-weight: 600; color: #909399; cursor: pointer; margin-bottom: -2px;">项目规约 (${projectRules.length})</button>` : ''}
</div>
<div id="tab-content-global" class="tab-pane">
<div style="margin-bottom: 16px;">
<button onclick="app.showAddRuleForm(null)" style="padding: 6px 12px; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">+ 添加全局规约</button>
</div>
<div style="max-height: 400px; overflow-y: auto;">
${renderRulesTable(globalRules, true)}
</div>
</div>
${this.currentProjectId ? `
<div id="tab-content-project" class="tab-pane" style="display: none;">
<div style="margin-bottom: 16px;">
<button onclick="app.showAddRuleForm(${this.currentProjectId})" style="padding: 6px 12px; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">+ 添加项目规约</button>
</div>
<div style="max-height: 400px; overflow-y: auto;">
${renderRulesTable(projectRules, false)}
</div>
</div>
` : ''}
</div>
`;
this.showModal('📜 协作规约', content);
// 绑定 Tab 切换事件
document.querySelectorAll('.modal-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
document.querySelectorAll('.modal-tab').forEach(t => {
t.classList.remove('active');
t.style.borderBottom = 'none';
t.style.color = '#909399';
});
e.target.classList.add('active');
e.target.style.borderBottom = '2px solid #667eea';
e.target.style.color = '#667eea';
const targetTab = e.target.dataset.tab;
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.style.display = 'none';
});
document.getElementById(`tab-content-${targetTab}`).style.display = 'block';
});
});
} catch (error) {
this.showToast('加载规约失败: ' + error.message, 'error');
}
}
showAddRuleForm(projectId) {
this.closeModal();
const formHTML = `
<form id="form-add-rule" style="display: flex; flex-direction: column; gap: 16px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">规约编号</label>
<input type="text" name="rule_no" placeholder="例如: R001" required
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">规约类型</label>
<select name="rule_type" required style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">
<option value="">请选择</option>
<option value="代码规范">代码规范</option>
<option value="提交规范">提交规范</option>
<option value="测试规范">测试规范</option>
<option value="文档规范">文档规范</option>
<option value="安全规范">安全规范</option>
<option value="其他">其他</option>
</select>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600;">内容描述</label>
<textarea name="description" placeholder="输入规约详细描述" rows="4" required
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;"></textarea>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<button type="button" onclick="app.closeModal()"
style="padding: 8px 16px; background: #f0f2f5; border-radius: 8px; border: none; cursor: pointer;">取消</button>
<button type="submit"
style="padding: 8px 16px; background: #667eea; color: white; border-radius: 8px; border: none; cursor: pointer;">添加</button>
</div>
</form>
`;
this.showModal(projectId ? '添加项目规约' : '添加全局规约', formHTML);
document.getElementById('form-add-rule').addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(e.target);
const rule = {
project_id: projectId,
rule_no: data.get('rule_no'),
rule_type: data.get('rule_type'),
description: data.get('description')
};
try {
await this.api.post('/rules', rule);
this.showToast('规约添加成功', 'success');
this.closeModal();
setTimeout(() => this.showGlobalRules(), 300);
} catch (error) {
this.showToast('添加失败: ' + error.message, 'error');
}
});
}
// ==================== 实时更新 ====================
startRealtimeUpdate() {
// 每30秒更新一次
setInterval(async () => {
await this.loadStats();
await this.loadAgents();
// 如果在看板页面,刷新看板
if (this.currentProjectId && !document.getElementById('content-task-board').classList.contains('hidden')) {
await this.loadKanbanBoard();
}
}, 30000);
}
// ==================== UI 工具方法 ====================
showModal(title, content) {
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-body').innerHTML = content;
document.getElementById('global-modal').classList.add('active');
}
closeModal() {
document.getElementById('global-modal').classList.remove('active');
}
showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// ==================== API 类 ====================
class API {
constructor(baseURL = '/api') {
this.baseURL = baseURL;
}
async request(method, endpoint, data = null) {
const url = this.baseURL + endpoint;
const options = {
method,
headers: {}
};
if (data) {
if (data instanceof FormData) {
options.body = data;
} else {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(data);
}
}
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(error.message || '请求失败');
}
return response.json();
}
async get(endpoint) {
return this.request('GET', endpoint);
}
async post(endpoint, data) {
return this.request('POST', endpoint, data);
}
async put(endpoint, data) {
return this.request('PUT', endpoint, data);
}
async delete(endpoint) {
return this.request('DELETE', endpoint);
}
}
// ==================== 状态管理类 ====================
class AppState {
constructor() {
this.projects = [];
this.tasks = [];
this.agents = [];
this.stats = {};
}
}
// ==================== 启动应用 ====================
let app;
document.addEventListener('DOMContentLoaded', () => {
app = new App();
});
</script>
</body>
</html>