2026-02-15 13:41:49 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
2026-02-24 15:04:25 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-02-15 13:41:49 +08:00
|
|
|
|
<style>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
/* ==================== 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 ==================== */
|
|
|
|
|
|
|
2026-02-15 13:41:49 +08:00
|
|
|
|
* {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
2026-02-15 13:41:49 +08:00
|
|
|
|
body {
|
2026-02-24 15:04:25 +08:00
|
|
|
|
font-family: var(--font-family-base);
|
|
|
|
|
|
font-size: var(--font-size-md);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
background: var(--bg-light);
|
|
|
|
|
|
line-height: 1.6;
|
2026-02-15 13:41:49 +08:00
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
2026-02-15 13:41:49 +08:00
|
|
|
|
button {
|
2026-02-24 15:04:25 +08:00
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
cursor: pointer;
|
2026-02-15 13:41:49 +08:00
|
|
|
|
border: none;
|
2026-02-24 15:04:25 +08:00
|
|
|
|
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%);
|
2026-02-15 13:41:49 +08:00
|
|
|
|
color: white;
|
2026-02-24 15:04:25 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 0 var(--spacing-lg);
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
|
|
|
|
|
z-index: 100;
|
2026-02-15 13:41:49 +08:00
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
.mobile-menu-btn {
|
2026-02-15 21:51:05 +08:00
|
|
|
|
display: none;
|
2026-02-24 15:04:25 +08:00
|
|
|
|
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;
|
2026-02-15 21:51:05 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 21:51:05 +08:00
|
|
|
|
.modal.active {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
2026-02-15 21:51:05 +08:00
|
|
|
|
.modal-content {
|
|
|
|
|
|
background: white;
|
2026-02-24 15:04:25 +08:00
|
|
|
|
border-radius: var(--radius-lg);
|
2026-02-15 21:51:05 +08:00
|
|
|
|
width: 90%;
|
2026-02-24 15:04:25 +08:00
|
|
|
|
max-width: 600px;
|
2026-02-15 21:51:05 +08:00
|
|
|
|
max-height: 80vh;
|
|
|
|
|
|
overflow-y: auto;
|
2026-02-15 13:41:49 +08:00
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-02-15 13:41:49 +08:00
|
|
|
|
}
|
2026-02-15 21:51:05 +08:00
|
|
|
|
|
2026-02-15 13:41:49 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
<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>
|
2026-02-15 13:41:49 +08:00
|
|
|
|
</header>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- App Body -->
|
|
|
|
|
|
<div class="app-body">
|
|
|
|
|
|
<!-- Sidebar -->
|
|
|
|
|
|
<aside id="sidebar">
|
|
|
|
|
|
<div class="sidebar-header">
|
|
|
|
|
|
<button id="btn-collapse">☰</button>
|
2026-02-15 13:41:49 +08:00
|
|
|
|
</div>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-02-15 21:51:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Main Content -->
|
|
|
|
|
|
<main id="main-content">
|
|
|
|
|
|
<!-- Loading -->
|
|
|
|
|
|
<div id="content-loading" class="hidden">
|
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
|
</div>
|
2026-02-15 21:51:05 +08:00
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
<!-- 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 id="rules-page-title">📜 协作规约</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="rules-tab-content">
|
|
|
|
|
|
<div class="rules-toolbar">
|
|
|
|
|
|
<button id="btn-add-rule" class="btn-add-rule">+ 添加规约</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rules-table-container">
|
|
|
|
|
|
<table class="rules-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th width="15%">项目</th>
|
|
|
|
|
|
<th width="12%">规约编号</th>
|
|
|
|
|
|
<th width="15%">规约类型</th>
|
|
|
|
|
|
<th width="43%">内容描述</th>
|
|
|
|
|
|
<th width="15%">操作</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody id="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">×</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">
|
2026-02-15 21:51:05 +08:00
|
|
|
|
<option value="initial">初始</option>
|
|
|
|
|
|
<option value="in_progress">进行中</option>
|
|
|
|
|
|
<option value="testing_pending">待测试</option>
|
|
|
|
|
|
<option value="testing">测试中</option>
|
|
|
|
|
|
<option value="completed">完成</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<label>类型</label>
|
|
|
|
|
|
<select id="task-type-select">
|
2026-02-15 21:51:05 +08:00
|
|
|
|
<option value="requirement">需求</option>
|
|
|
|
|
|
<option value="design">设计</option>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
<option value="development">开发</option>
|
2026-02-15 21:51:05 +08:00
|
|
|
|
<option value="testing">测试</option>
|
|
|
|
|
|
<option value="deployment">部署</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="detail-section">
|
2026-02-15 21:51:05 +08:00
|
|
|
|
<label>优先级</label>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
<select id="task-priority-select">
|
2026-02-15 21:51:05 +08:00
|
|
|
|
<option value="low">低</option>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
<option value="normal">普通</option>
|
2026-02-15 21:51:05 +08:00
|
|
|
|
<option value="high">高</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
|
<label>执行者</label>
|
|
|
|
|
|
<select id="task-assignee-select">
|
|
|
|
|
|
<option value="">未分配</option>
|
|
|
|
|
|
<!-- 动态加载智能体列表 -->
|
|
|
|
|
|
</select>
|
2026-02-15 21:51:05 +08:00
|
|
|
|
</div>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
<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()">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body" id="modal-body">
|
|
|
|
|
|
<!-- 动态内容 -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-15 21:51:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
2026-02-15 13:41:49 +08:00
|
|
|
|
<script>
|
2026-02-24 15:04:25 +08:00
|
|
|
|
// ==================== 核心应用类 ====================
|
|
|
|
|
|
|
|
|
|
|
|
class App {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.api = new API();
|
|
|
|
|
|
this.state = new AppState();
|
|
|
|
|
|
this.currentTaskId = null;
|
|
|
|
|
|
this.currentProjectId = null;
|
2026-02-15 13:41:49 +08:00
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
this.init();
|
2026-02-15 13:41:49 +08:00
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
async init() {
|
|
|
|
|
|
// 默认显示 Dashboard
|
|
|
|
|
|
setTimeout(() => this.showDashboard(), 100);
|
2026-02-15 13:41:49 +08:00
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
// 初始化事件监听
|
|
|
|
|
|
this.bindEvents();
|
2026-02-15 13:41:49 +08:00
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
// 加载初始数据
|
|
|
|
|
|
await this.loadProjects();
|
|
|
|
|
|
await this.loadAgents();
|
|
|
|
|
|
await this.loadStats();
|
2026-02-15 13:41:49 +08:00
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
// 绑定子任务按钮
|
|
|
|
|
|
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 isGlobalRules = !document.getElementById('content-rules').classList.contains('hidden') &&
|
|
|
|
|
|
this.currentRuleContext === 'global';
|
|
|
|
|
|
if (isGlobalRules) {
|
|
|
|
|
|
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);
|
2026-02-15 13:41:49 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
this.state.projects.forEach(project => {
|
|
|
|
|
|
const li = document.createElement('li');
|
|
|
|
|
|
li.className = 'project-item';
|
|
|
|
|
|
li.style.cssText = 'list-style: none;';
|
|
|
|
|
|
|
|
|
|
|
|
// 项目主条目
|
|
|
|
|
|
const projectHeader = document.createElement('div');
|
|
|
|
|
|
projectHeader.className = 'project-header';
|
|
|
|
|
|
projectHeader.style.cssText = 'display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; border-radius: 6px; transition: background 0.2s;';
|
|
|
|
|
|
|
|
|
|
|
|
// 展开/收起图标
|
|
|
|
|
|
const toggleIcon = document.createElement('span');
|
|
|
|
|
|
toggleIcon.textContent = isCollapsed ? '' : '▶';
|
|
|
|
|
|
toggleIcon.style.cssText = 'font-size: 10px; color: #909399; transition: transform 0.2s; flex-shrink: 0; width: 12px;';
|
|
|
|
|
|
toggleIcon.className = 'project-toggle';
|
|
|
|
|
|
|
|
|
|
|
|
// 项目名称
|
|
|
|
|
|
const nameSpan = document.createElement('span');
|
|
|
|
|
|
nameSpan.textContent = project.name;
|
|
|
|
|
|
nameSpan.style.flex = '1';
|
|
|
|
|
|
nameSpan.style.fontSize = '13px';
|
|
|
|
|
|
nameSpan.style.fontWeight = '500';
|
|
|
|
|
|
|
|
|
|
|
|
// 删除按钮
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
projectHeader.appendChild(toggleIcon);
|
|
|
|
|
|
projectHeader.appendChild(nameSpan);
|
|
|
|
|
|
projectHeader.appendChild(deleteBtn);
|
|
|
|
|
|
|
|
|
|
|
|
// 子项目列表
|
|
|
|
|
|
const subList = document.createElement('ul');
|
|
|
|
|
|
subList.className = 'project-sub-items';
|
|
|
|
|
|
subList.style.cssText = 'list-style: none; padding-left: 24px; margin: 4px 0 0 0; display: none;';
|
|
|
|
|
|
|
|
|
|
|
|
// 看板子项
|
|
|
|
|
|
const kanbanItem = document.createElement('li');
|
|
|
|
|
|
kanbanItem.style.cssText = 'padding: 6px 12px; font-size: 12px; cursor: pointer; border-radius: 4px; transition: background 0.2s; margin-bottom: 2px;';
|
|
|
|
|
|
kanbanItem.textContent = '看板';
|
|
|
|
|
|
// 判断是否是当前项目的看板视图
|
|
|
|
|
|
const isKanban = project.id === this.currentProjectId &&
|
|
|
|
|
|
document.getElementById('content-task-board').classList.contains('hidden') === false;
|
|
|
|
|
|
if (isKanban) {
|
|
|
|
|
|
kanbanItem.style.background = 'rgba(102, 126, 234, 0.1)';
|
|
|
|
|
|
kanbanItem.style.color = '#667eea';
|
|
|
|
|
|
kanbanItem.style.fontWeight = '600';
|
|
|
|
|
|
}
|
|
|
|
|
|
kanbanItem.addEventListener('click', (e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
this.selectProject(project.id);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 项目规约子项
|
|
|
|
|
|
const rulesItem = document.createElement('li');
|
|
|
|
|
|
rulesItem.style.cssText = 'padding: 6px 12px; font-size: 12px; cursor: pointer; border-radius: 4px; transition: background 0.2s;';
|
|
|
|
|
|
rulesItem.textContent = '项目规约';
|
|
|
|
|
|
// 判断是否是当前项目的规约视图
|
|
|
|
|
|
const isProjectRules = project.id === this.currentProjectId &&
|
|
|
|
|
|
document.getElementById('content-rules').classList.contains('hidden') === false &&
|
|
|
|
|
|
this.currentRuleContext === 'project';
|
|
|
|
|
|
if (isProjectRules) {
|
|
|
|
|
|
rulesItem.style.background = 'rgba(102, 126, 234, 0.1)';
|
|
|
|
|
|
rulesItem.style.color = '#667eea';
|
|
|
|
|
|
rulesItem.style.fontWeight = '600';
|
|
|
|
|
|
}
|
|
|
|
|
|
rulesItem.addEventListener('click', (e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
this.showProjectRules(project.id);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
subList.appendChild(kanbanItem);
|
|
|
|
|
|
subList.appendChild(rulesItem);
|
|
|
|
|
|
|
|
|
|
|
|
// 展开/收起逻辑
|
|
|
|
|
|
// 如果是当前项目,自动展开;否则保持之前的状态
|
|
|
|
|
|
if (!this.projectExpandedState) {
|
|
|
|
|
|
this.projectExpandedState = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 当前项目默认展开
|
|
|
|
|
|
if (project.id === this.currentProjectId && this.projectExpandedState[project.id] === undefined) {
|
|
|
|
|
|
this.projectExpandedState[project.id] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isExpanded = this.projectExpandedState[project.id] === true;
|
|
|
|
|
|
if (isExpanded && !isCollapsed) {
|
|
|
|
|
|
subList.style.display = 'block';
|
|
|
|
|
|
toggleIcon.style.transform = 'rotate(90deg)';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
projectHeader.addEventListener('click', () => {
|
|
|
|
|
|
if (isCollapsed) return;
|
|
|
|
|
|
// 切换展开状态
|
|
|
|
|
|
const newState = !this.projectExpandedState[project.id];
|
|
|
|
|
|
this.projectExpandedState[project.id] = newState;
|
|
|
|
|
|
subList.style.display = newState ? 'block' : 'none';
|
|
|
|
|
|
toggleIcon.style.transform = newState ? 'rotate(90deg)' : 'rotate(0deg)';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
li.appendChild(projectHeader);
|
|
|
|
|
|
li.appendChild(subList);
|
|
|
|
|
|
list.appendChild(li);
|
|
|
|
|
|
});
|
2026-02-15 21:51:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
async deleteProject(projectId, projectName) {
|
|
|
|
|
|
if (!confirm(`确定要删除项目"${projectName}"吗?\n\n删除后该项目下的所有任务也将被删除,此操作不可恢复!`)) {
|
2026-02-15 13:41:49 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-15 21:51:05 +08:00
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
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');
|
2026-02-15 21:51:05 +08:00
|
|
|
|
|
|
|
|
|
|
if (agents.length === 0) {
|
2026-02-24 15:04:25 +08:00
|
|
|
|
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 32px; color: #909399;">暂无数据</td></tr>';
|
2026-02-15 21:51:05 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
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);
|
2026-02-15 21:51:05 +08:00
|
|
|
|
});
|
2026-02-24 15:04:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
2026-02-15 21:51:05 +08:00
|
|
|
|
});
|
2026-02-24 15:04:25 +08:00
|
|
|
|
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');
|
2026-02-15 21:51:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-02-24 15:04:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 智能体注册表单 ====================
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
2026-02-15 21:51:05 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== 工具方法 ====================
|
|
|
|
|
|
|
|
|
|
|
|
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.currentRuleContext = null; // 清除规约上下文,表示不在规约页面
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏所有其他内容
|
|
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
|
|
// 重新渲染侧边栏以更新高亮
|
|
|
|
|
|
this.renderProjects();
|
|
|
|
|
|
|
|
|
|
|
|
// 加载任务
|
|
|
|
|
|
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.showRulesPage('global');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async showRulesPage(context = 'global') {
|
|
|
|
|
|
// 隐藏其他内容
|
|
|
|
|
|
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.currentRuleContext = context;
|
|
|
|
|
|
this.currentProjectId = context === 'global' ? null : this.currentProjectId;
|
|
|
|
|
|
this.renderProjects();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新标题
|
|
|
|
|
|
document.getElementById('rules-page-title').textContent =
|
|
|
|
|
|
context === 'global' ? '📜 全局规约' : '📜 协作规约';
|
|
|
|
|
|
|
|
|
|
|
|
// 加载规约数据
|
|
|
|
|
|
await this.loadRulesData();
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定添加按钮
|
|
|
|
|
|
this.bindAddRuleButton();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async showProjectRules(projectId) {
|
|
|
|
|
|
this.currentProjectId = projectId;
|
|
|
|
|
|
this.currentRuleContext = 'project';
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏其他内容
|
|
|
|
|
|
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.renderProjects();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新标题为项目名称
|
|
|
|
|
|
const project = this.state.projects.find(p => p.id === projectId);
|
|
|
|
|
|
document.getElementById('rules-page-title').textContent =
|
|
|
|
|
|
project ? `📜 ${project.name} - 项目规约` : '📜 项目规约';
|
|
|
|
|
|
|
|
|
|
|
|
// 加载项目规约
|
|
|
|
|
|
await this.loadRulesData();
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定添加按钮
|
|
|
|
|
|
this.bindAddRuleButton();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadRulesData() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let rules = [];
|
|
|
|
|
|
if (this.currentRuleContext === 'global') {
|
|
|
|
|
|
rules = await this.api.get('/rules/global');
|
|
|
|
|
|
} else if (this.currentProjectId) {
|
|
|
|
|
|
rules = await this.api.get(`/rules/project/${this.currentProjectId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.renderRulesTable(rules);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载规约失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderRulesTable(rules) {
|
|
|
|
|
|
const tbody = document.getElementById('rules-body');
|
|
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
if (!rules || rules.length === 0) {
|
|
|
|
|
|
tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; padding: 32px; color: #909399;">暂无规约</td></tr>`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
rules.forEach(rule => {
|
|
|
|
|
|
const tr = document.createElement('tr');
|
|
|
|
|
|
|
|
|
|
|
|
// 显示项目名称
|
|
|
|
|
|
let projectName = '全局';
|
|
|
|
|
|
if (rule.project_id && rule.project_id !== 'global') {
|
|
|
|
|
|
const project = this.state.projects.find(p => p.id == rule.project_id);
|
|
|
|
|
|
projectName = project ? project.name : `项目${rule.project_id}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tr.innerHTML = `
|
|
|
|
|
|
<td><span style="padding: 2px 8px; background: ${rule.project_id && rule.project_id !== 'global' ? '#e7f5ff' : '#f0f0f0'}; border-radius: 4px; font-size: 12px; font-weight: 600;">${this.escapeHtml(projectName)}</span></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})">删除</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
`;
|
|
|
|
|
|
tbody.appendChild(tr);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bindAddRuleButton() {
|
|
|
|
|
|
const addBtn = document.getElementById('btn-add-rule');
|
|
|
|
|
|
if (addBtn) {
|
|
|
|
|
|
const newBtn = addBtn.cloneNode(true);
|
|
|
|
|
|
addBtn.parentNode.replaceChild(newBtn, addBtn);
|
|
|
|
|
|
newBtn.addEventListener('click', () => {
|
|
|
|
|
|
if (this.currentRuleContext === 'global') {
|
|
|
|
|
|
this.showAddRuleForm('global');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showAddRuleForm(this.currentProjectId);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async deleteRule(ruleId) {
|
|
|
|
|
|
if (!confirm('确定要删除这条规约吗?')) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.api.delete(`/rules/${ruleId}`);
|
|
|
|
|
|
this.showToast('规约已删除', 'success');
|
|
|
|
|
|
await this.loadRulesData();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.showToast('删除失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async editRule(ruleId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const rule = await this.api.get(`/rules/${ruleId}`);
|
|
|
|
|
|
|
|
|
|
|
|
const formHTML = `
|
|
|
|
|
|
<form id="form-edit-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" value="${this.escapeHtml(rule.rule_no)}" 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="项目管理规范" ${rule.rule_type === '项目管理规范' ? 'selected' : ''}>项目管理规范</option>
|
|
|
|
|
|
<option value="需求管理规范" ${rule.rule_type === '需求管理规范' ? 'selected' : ''}>需求管理规范</option>
|
|
|
|
|
|
<option value="设计规范" ${rule.rule_type === '设计规范' ? 'selected' : ''}>设计规范</option>
|
|
|
|
|
|
<option value="代码编写规范" ${rule.rule_type === '代码编写规范' ? 'selected' : ''}>代码编写规范</option>
|
|
|
|
|
|
<option value="系统部署规约" ${rule.rule_type === '系统部署规约' ? 'selected' : ''}>系统部署规约</option>
|
|
|
|
|
|
<option value="单元测试规范" ${rule.rule_type === '单元测试规范' ? 'selected' : ''}>单元测试规范</option>
|
|
|
|
|
|
<option value="集成测试规范" ${rule.rule_type === '集成测试规范' ? 'selected' : ''}>集成测试规范</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label style="display: block; margin-bottom: 8px; font-weight: 600;">内容描述</label>
|
|
|
|
|
|
<textarea name="description" rows="4" required
|
|
|
|
|
|
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px;">${this.escapeHtml(rule.description)}</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('编辑规约', formHTML);
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('form-edit-rule').addEventListener('submit', async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const data = new FormData(e.target);
|
|
|
|
|
|
const updated = {
|
|
|
|
|
|
rule_no: data.get('rule_no'),
|
|
|
|
|
|
rule_type: data.get('rule_type'),
|
|
|
|
|
|
description: data.get('description')
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.api.put(`/rules/${ruleId}`, updated);
|
|
|
|
|
|
this.showToast('规约已更新', 'success');
|
|
|
|
|
|
this.closeModal();
|
|
|
|
|
|
await this.loadRulesData();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.showToast('更新失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.showToast('加载规约失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showAddRuleForm(projectIdOrContext) {
|
|
|
|
|
|
const isGlobal = projectIdOrContext === 'global';
|
|
|
|
|
|
const projectId = isGlobal ? null : projectIdOrContext;
|
|
|
|
|
|
|
|
|
|
|
|
let projectDisplay = '全局';
|
|
|
|
|
|
if (!isGlobal && projectId) {
|
|
|
|
|
|
const project = this.state.projects.find(p => p.id == projectId);
|
|
|
|
|
|
projectDisplay = project ? project.name : `项目${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" value="${this.escapeHtml(projectDisplay)}" disabled
|
|
|
|
|
|
style="width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; background: #f5f5f5; color: #666;">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<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(isGlobal ? '添加全局规约' : '添加项目规约', formHTML);
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('form-add-rule').addEventListener('submit', async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const data = new FormData(e.target);
|
|
|
|
|
|
const rule = {
|
|
|
|
|
|
project_id: isGlobal ? 'global' : 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.loadRulesData();
|
|
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
2026-02-15 21:51:05 +08:00
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
// ==================== API 类 ====================
|
|
|
|
|
|
|
|
|
|
|
|
class API {
|
|
|
|
|
|
constructor(baseURL = '/api') {
|
|
|
|
|
|
this.baseURL = baseURL;
|
2026-02-15 13:41:49 +08:00
|
|
|
|
}
|
2026-02-15 21:51:05 +08:00
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-02-15 21:51:05 +08:00
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
2026-02-15 21:51:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 15:04:25 +08:00
|
|
|
|
async get(endpoint) {
|
|
|
|
|
|
return this.request('GET', endpoint);
|
2026-02-15 21:51:05 +08:00
|
|
|
|
}
|
2026-02-24 15:04:25 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
2026-02-15 21:51:05 +08:00
|
|
|
|
});
|
2026-02-15 13:41:49 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|