Files
work-1 01f43d8da5 feat: v3.1 - 统一规约视图、项目树导航、移动端支持
主要更新:
- 统一规约管理视图(全局/项目规约共享表格)
- 项目树形导航(看板/项目规约子菜单)
- 智能高亮与状态持久化
- 移动端响应式支持(汉堡菜单)
- 规约类型更新为7个专业分类
- 文件清理(85→15文件,减少82%)

修复问题:
- 修复创建项目功能(JavaScript语法错误)
- 修复协作规约错误高亮
- 修复看板需要点击2次才高亮
- 修复项目树自动收起问题

新增工具:
- debug.html(项目创建测试)
- test.html(移动端测试)
- status.html(系统诊断)

技术改进:
- 上下文状态管理(currentRuleContext)
- 展开状态持久化(projectExpandedState)
- 触摸事件支持
- 移除console.log提升兼容性
2026-02-24 15:04:25 +08:00

2954 lines
96 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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');
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleMobileSidebar();
});
// 添加触摸事件支持
mobileMenuBtn.addEventListener('touchstart', (e) => {
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');
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>