diff --git a/XNSimHtml/components/todo-component.js b/XNSimHtml/components/todo-component.js index a37c0c8..86e5a48 100644 --- a/XNSimHtml/components/todo-component.js +++ b/XNSimHtml/components/todo-component.js @@ -1,3 +1,8 @@ +import TodoService from './todo/todo-service.js'; +import TodoModal from './todo/todo-modal.js'; +import TodoTree from './todo/todo-tree.js'; +import TodoList from './todo/todo-list.js'; + class TodoComponent extends HTMLElement { static { // 添加静态初始化标志 @@ -13,14 +18,13 @@ class TodoComponent extends HTMLElement { this.expandedProjects = new Set(); this.users = []; this.currentUser = null; - this.editingTodo = null; // 当前正在编辑的待办事项 - this.isInitialized = false; // 添加初始化标志 - this.showCompleted = true; // 添加显示已完成的状态变量 + this.editingTodo = null; + this.isInitialized = false; + this.showCompleted = true; } // 初始化组件 async initialize() { - // 检查是否正在初始化 if (TodoComponent.isInitializing) { return; } @@ -49,11 +53,7 @@ class TodoComponent extends HTMLElement { async fetchTodos() { try { - const response = await fetch('/api/todos'); - if (!response.ok) { - throw new Error('获取待办事项失败'); - } - this.todos = await response.json(); + this.todos = await TodoService.fetchTodos(); } catch (error) { console.error('获取待办事项时发生错误:', error); this.showError('获取待办事项失败,请稍后重试'); @@ -62,12 +62,7 @@ class TodoComponent extends HTMLElement { async fetchUsers() { try { - const response = await fetch('/api/users'); - if (!response.ok) { - throw new Error('获取用户数据失败'); - } - const data = await response.json(); - this.users = data.users || []; + this.users = await TodoService.fetchUsers(); } catch (error) { console.error('获取用户数据时发生错误:', error); this.showError('获取用户数据失败,请稍后重试'); @@ -95,247 +90,10 @@ class TodoComponent extends HTMLElement { this.shadowRoot.appendChild(errorDiv); } - // 构建项目树结构 - buildProjectTree() { - const tree = {}; - this.todos.forEach(todo => { - const project = todo.project || '其它'; - const subproject = todo.subproject || '其它'; - - if (!tree[project]) { - tree[project] = { - name: project, - subprojects: {} - }; - } - if (!tree[project].subprojects[subproject]) { - tree[project].subprojects[subproject] = { - name: subproject, - todos: [] - }; - } - tree[project].subprojects[subproject].todos.push(todo); - }); - return tree; - } - - // 渲染项目树 - renderProjectTree(tree) { - const treeContainer = document.createElement('div'); - treeContainer.className = 'project-tree'; - - // 创建树内容容器 - const treeContent = document.createElement('div'); - treeContent.className = 'tree-content'; - - // 添加新增待办按钮 - const addTodoButton = document.createElement('button'); - addTodoButton.className = 'add-todo-button'; - addTodoButton.textContent = '新增待办'; - addTodoButton.addEventListener('click', () => this.showNewTodoModal()); - treeContent.appendChild(addTodoButton); - - Object.values(tree).forEach(project => { - const projectNode = document.createElement('div'); - projectNode.className = 'project-node'; - - const projectHeader = document.createElement('div'); - projectHeader.className = 'project-header'; - projectHeader.innerHTML = ` -
- - ${project.name} - (${Object.values(project.subprojects).reduce((sum, sub) => - sum + sub.todos.length, 0)}) -
- `; - - projectHeader.addEventListener('click', () => { - this.selectedProject = project.name; - this.selectedSubproject = null; - this.render(); - }); - - const expandButton = projectHeader.querySelector('.expand-icon'); - expandButton.addEventListener('click', (e) => { - e.stopPropagation(); - if (this.expandedProjects.has(project.name)) { - this.expandedProjects.delete(project.name); - } else { - this.expandedProjects.add(project.name); - } - this.render(); - }); - - const subprojectsContainer = document.createElement('div'); - subprojectsContainer.className = 'subprojects-container'; - subprojectsContainer.style.display = this.expandedProjects.has(project.name) ? 'block' : 'none'; - - Object.values(project.subprojects).forEach(subproject => { - const subprojectNode = document.createElement('div'); - subprojectNode.className = 'subproject-node'; - subprojectNode.innerHTML = ` - ${subproject.name} - (${subproject.todos.length}) - `; - - subprojectNode.addEventListener('click', (e) => { - e.stopPropagation(); - this.selectedProject = project.name; - this.selectedSubproject = subproject.name; - this.render(); - }); - - subprojectsContainer.appendChild(subprojectNode); - }); - - projectNode.appendChild(projectHeader); - projectNode.appendChild(subprojectsContainer); - treeContent.appendChild(projectNode); - }); - - // 创建底部控制区域 - const bottomControls = document.createElement('div'); - bottomControls.className = 'bottom-controls'; - - // 添加显示已完成复选框 - const showCompletedContainer = document.createElement('div'); - showCompletedContainer.className = 'show-completed-container'; - showCompletedContainer.innerHTML = ` - - - `; - - // 添加复选框事件监听 - const checkbox = showCompletedContainer.querySelector('input'); - checkbox.addEventListener('change', (e) => { - this.showCompleted = e.target.checked; - this.render(); - }); - - bottomControls.appendChild(showCompletedContainer); - - // 添加显示全部按钮 - const showAllButton = document.createElement('button'); - showAllButton.className = 'show-all-button'; - showAllButton.textContent = '显示全部'; - showAllButton.addEventListener('click', () => { - this.selectedProject = null; - this.selectedSubproject = null; - this.render(); - }); - bottomControls.appendChild(showAllButton); - - // 将树内容和底部控制区域添加到树容器 - treeContainer.appendChild(treeContent); - treeContainer.appendChild(bottomControls); - - return treeContainer; - } - - // 过滤待办事项 - filterTodos() { - if (!this.currentUser) { - console.log('未获取到当前用户信息,无法进行筛选'); - return []; - } - - // 首先根据项目/子项目筛选 - let filteredTodos = this.todos; - if (this.selectedProject) { - filteredTodos = filteredTodos.filter(todo => { - const matchesProject = todo.project === this.selectedProject; - if (!this.selectedSubproject) { - return matchesProject; - } - return matchesProject && todo.subproject === this.selectedSubproject; - }); - } - - // 根据复选框状态筛选 - if (!this.showCompleted) { - filteredTodos = filteredTodos.filter(todo => !todo.completed); - } - - // 根据用户等级进行筛选 - const userLevel = this.currentUser.level || 0; - - // 等级4可以看到所有待办事项 - if (userLevel === 4) { - return filteredTodos; - } - - // 等级3不能看到创建人和执行人都是等级4的待办事项 - if (userLevel === 3) { - return filteredTodos.filter(todo => { - const creator = this.users.find(u => u.username === todo.adduser); - const executor = this.users.find(u => u.username === todo.exeuser); - - // 如果找不到用户信息,默认显示 - if (!creator || !executor) return true; - - // 只要创建人或执行人不是等级4,就显示 - return !(creator.level === 4 && executor.level === 4); - }); - } - - // 等级2或更低只能看到创建人或执行人是自己的待办事项 - return filteredTodos.filter(todo => { - return todo.adduser === this.currentUser.username || - todo.exeuser === this.currentUser.username; - }); - } - - // 显示编辑计划时间对话框 - showEditScheduleModal(todo) { - this.editingTodo = todo; - const { modal, style } = this.createModal('schedule'); - - // 设置日期时间选择器的初始值 - const scheduleInput = modal.querySelector('#schedule'); - const scheduleDate = new Date(todo.sche_time); - scheduleInput.value = scheduleDate.toISOString().slice(0, 16); - - // 添加保存按钮事件 - const saveButton = modal.querySelector('.save-button'); - saveButton.addEventListener('click', async () => { - const scheduleTime = scheduleInput.value; - if (scheduleTime && this.editingTodo) { - try { - const response = await fetch(`/api/todos/${this.editingTodo.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - ...this.editingTodo, - sche_time: scheduleTime - }) - }); - - if (!response.ok) { - throw new Error('更新计划时间失败'); - } - - // 更新本地数据 - this.editingTodo.sche_time = scheduleTime; - this.render(); - modal.remove(); - style.remove(); - } catch (error) { - console.error('更新计划时间时发生错误:', error); - this.showError('更新计划时间失败,请稍后重试'); - } - } - }); - - document.body.appendChild(modal); - } - // 显示编辑执行人对话框 showEditExecutorModal(todo) { this.editingTodo = todo; - const { modal, style } = this.createModal('executor'); + const { modal, style } = TodoModal.createModal('executor'); // 获取当前用户等级 const currentUserLevel = this.currentUser.access_level || 0; @@ -367,21 +125,11 @@ class TodoComponent extends HTMLElement { const executor = executorSelect.value; if (executor && this.editingTodo) { try { - const response = await fetch(`/api/todos/${this.editingTodo.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - ...this.editingTodo, - exeuser: executor - }) + await TodoService.updateTodo(this.editingTodo.id, { + ...this.editingTodo, + exeuser: executor }); - if (!response.ok) { - throw new Error('更新执行人失败'); - } - // 更新本地数据 this.editingTodo.exeuser = executor; this.render(); @@ -397,9 +145,45 @@ class TodoComponent extends HTMLElement { document.body.appendChild(modal); } + // 显示编辑计划时间对话框 + showEditScheduleModal(todo) { + this.editingTodo = todo; + const { modal, style } = TodoModal.createModal('schedule'); + + // 设置日期时间选择器的初始值 + const scheduleInput = modal.querySelector('#schedule'); + const scheduleDate = new Date(todo.sche_time); + scheduleInput.value = scheduleDate.toISOString().slice(0, 16); + + // 添加保存按钮事件 + const saveButton = modal.querySelector('.save-button'); + saveButton.addEventListener('click', async () => { + const scheduleTime = scheduleInput.value; + if (scheduleTime && this.editingTodo) { + try { + await TodoService.updateTodo(this.editingTodo.id, { + ...this.editingTodo, + sche_time: scheduleTime + }); + + // 更新本地数据 + this.editingTodo.sche_time = scheduleTime; + this.render(); + modal.remove(); + style.remove(); + } catch (error) { + console.error('更新计划时间时发生错误:', error); + this.showError('更新计划时间失败,请稍后重试'); + } + } + }); + + document.body.appendChild(modal); + } + // 显示新增待办对话框 showNewTodoModal() { - const { modal, style } = this.createModal('new-todo'); + const { modal, style } = TodoModal.createModal('new-todo'); // 设置计划时间的默认值为当前时间 const scheduleInput = modal.querySelector('#schedule'); @@ -428,7 +212,7 @@ class TodoComponent extends HTMLElement { }); // 获取所有项目和子项目 - const projectTree = this.buildProjectTree(); + const projectTree = TodoTree.buildProjectTree(this.todos); const projectInput = modal.querySelector('#project'); const subprojectInput = modal.querySelector('#subproject'); const projectOptions = modal.querySelector('#project-options'); @@ -524,29 +308,18 @@ class TodoComponent extends HTMLElement { } try { - const response = await fetch('/api/todos', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - project, - subproject, - title, - text, - sche_time: scheduleTime, - exeuser: executor, - adduser: this.currentUser.username, - completed: false, - status: 'pending' // 添加初始状态 - }) + await TodoService.createTodo({ + project, + subproject, + title, + text, + sche_time: scheduleTime, + exeuser: executor, + adduser: this.currentUser.username, + completed: false, + status: 'pending' }); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || '创建待办失败'); - } - // 重新加载待办列表并渲染 await this.fetchTodos(); this.render(); @@ -561,706 +334,57 @@ class TodoComponent extends HTMLElement { document.body.appendChild(modal); } - // 创建模态对话框 - createModal(type) { - // 创建样式 - const style = document.createElement('style'); - style.textContent = ` - .modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; - } - .modal-content { - background-color: white; - border-radius: 8px; - width: 400px; - max-width: 90%; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - position: relative; - margin: auto; - } - .modal-header { - padding: 15px; - border-bottom: 1px solid #e9ecef; - display: flex; - justify-content: space-between; - align-items: center; - } - .modal-header h3 { - margin: 0; - color: #212529; - } - .close-button { - background: none; - border: none; - font-size: 1.5em; - color: #6c757d; - cursor: pointer; - padding: 0; - line-height: 1; - } - .modal-body { - padding: 15px; - } - .modal-footer { - padding: 15px; - border-top: 1px solid #e9ecef; - display: flex; - justify-content: flex-end; - gap: 10px; - } - .form-group { - margin-bottom: 15px; - } - .form-group label { - display: block; - margin-bottom: 5px; - color: #495057; - } - .form-group input[type="text"], - .form-group select { - width: 100%; - padding: 8px 10px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; - margin-bottom: 5px; - background-color: white; - } - .form-group input[type="text"]:focus, - .form-group select:focus { - border-color: #7986E7; - outline: none; - box-shadow: 0 0 0 2px rgba(121, 134, 231, 0.2); - } - .form-group select:disabled { - background-color: #f8f9fa; - cursor: not-allowed; - } - .form-group select[contenteditable="true"] { - cursor: text; - } - /* 添加 datalist 相关样式 */ - .form-group input[list] { - width: 100%; - padding: 8px 10px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; - margin-bottom: 5px; - background-color: white; - } - .form-group input[list]:focus { - border-color: #7986E7; - outline: none; - box-shadow: 0 0 0 2px rgba(121, 134, 231, 0.2); - } - .form-group datalist { - width: 100%; - padding: 8px 10px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; - background-color: white; - } - /* 自定义 datalist 下拉列表样式 */ - .form-group input[list]::-webkit-calendar-picker-indicator { - display: none; - } - .form-group input[list]::-webkit-list-button { - display: none; - } - .form-group input[list]::-webkit-datetime-edit { - padding: 0; - } - .form-group input[list]::-webkit-datetime-edit-fields-wrapper { - padding: 0; - } - .form-group input[list]::-webkit-datetime-edit-text { - padding: 0 2px; - } - .form-group input[list]::-webkit-datetime-edit-hour-field, - .form-group input[list]::-webkit-datetime-edit-minute-field, - .form-group input[list]::-webkit-datetime-edit-second-field, - .form-group input[list]::-webkit-datetime-edit-ampm-field { - padding: 0 2px; - } - /* 自定义下拉列表样式 */ - .form-group input[list] + datalist { - position: absolute; - background: white; - border: 1px solid #ddd; - border-radius: 4px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - max-height: 200px; - overflow-y: auto; - z-index: 1000; - } - .form-group input[list] + datalist option { - padding: 8px 10px; - cursor: pointer; - font-size: 14px; - color: #333; - } - .form-group input[list] + datalist option:hover { - background-color: #f5f5f5; - } - .save-button, .cancel-button { - padding: 8px 16px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - } - .save-button { - background-color: #7986E7; - color: white; - } - .save-button:hover { - background-color: #6875D6; - } - .cancel-button { - background-color: #6c757d; - color: white; - } - .cancel-button:hover { - background-color: #5a6268; - } - /* 日期时间选择器特定样式 */ - input[type="datetime-local"] { - font-family: inherit; - color: #333; - } - input[type="datetime-local"]::-webkit-calendar-picker-indicator { - cursor: pointer; - padding: 4px; - margin-right: 4px; - opacity: 0.6; - } - input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover { - opacity: 1; - } - input[type="datetime-local"]::-webkit-datetime-edit { - padding: 0; - } - input[type="datetime-local"]::-webkit-datetime-edit-fields-wrapper { - padding: 0; - } - input[type="datetime-local"]::-webkit-datetime-edit-text { - padding: 0 2px; - } - input[type="datetime-local"]::-webkit-datetime-edit-hour-field, - input[type="datetime-local"]::-webkit-datetime-edit-minute-field, - input[type="datetime-local"]::-webkit-datetime-edit-second-field, - input[type="datetime-local"]::-webkit-datetime-edit-ampm-field { - padding: 0 2px; - } - .form-group textarea { - width: 100%; - min-height: 100px; - padding: 8px 10px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 14px; - resize: vertical; - } - .form-group textarea:focus { - border-color: #7986E7; - outline: none; - box-shadow: 0 0 0 2px rgba(121, 134, 231, 0.2); - } - /* 自定义下拉列表样式 */ - .custom-select { - position: relative; - width: 100%; - } - .select-options { - display: none; - position: absolute; - top: 100%; - left: 0; - right: 0; - background: white; - border: 1px solid #7986E7; - border-radius: 4px; - box-shadow: 0 4px 8px rgba(121, 134, 231, 0.2); - max-height: 200px; - overflow-y: auto; - z-index: 1000; - margin-top: 2px; - } - .select-option { - padding: 8px 10px; - cursor: pointer; - font-size: 14px; - color: #333; - transition: all 0.2s ease; - } - .select-option:hover { - background-color: #7986E7; - color: white; - } - .select-option:active { - background-color: #6875D6; - } - /* 添加滚动条样式 */ - .select-options::-webkit-scrollbar { - width: 6px; - } - .select-options::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 3px; - } - .select-options::-webkit-scrollbar-thumb { - background: #7986E7; - border-radius: 3px; - } - .select-options::-webkit-scrollbar-thumb:hover { - background: #6875D6; - } - .show-completed-container { - margin: 10px 0; - padding: 8px; - display: flex; - align-items: center; - gap: 8px; - background: #f8f9fa; - border-radius: 4px; - } - .show-completed-container input[type="checkbox"] { - cursor: pointer; - width: 16px; - height: 16px; - } - .show-completed-container label { - cursor: pointer; - user-select: none; - color: #495057; - font-size: 14px; - } - .show-completed-container:hover { - background: #e9ecef; - } - .show-all-button { - width: 100%; - padding: 8px 16px; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: background-color 0.2s; - } - .show-all-button:hover { - background-color: #0056b3; - } - .show-all-button:active { - background-color: #004085; - } - .project-node { - margin-bottom: 10px; - } - .project-header { - padding: 8px; - background: #e9ecef; - border-radius: 4px; - cursor: pointer; - } - .project-header-content { - display: flex; - align-items: center; - gap: 8px; - } - .project-header:hover { - background: #dee2e6; - } - .expand-icon { - font-size: 12px; - transition: transform 0.2s; - color: #495057; - } - .expand-icon.expanded { - transform: rotate(90deg); - } - .subprojects-container { - margin-left: 20px; - margin-top: 5px; - } - .subproject-node { - padding: 6px 8px; - cursor: pointer; - border-radius: 4px; - display: flex; - justify-content: space-between; - align-items: center; - } - .subproject-node:hover { - background: #e9ecef; - } - .todo-count { - font-size: 0.9em; - color: #6c757d; - } - .todo-item { - background: #fff; - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 15px; - margin-bottom: 15px; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); - } - .todo-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - padding-bottom: 8px; - border-bottom: 1px solid #e9ecef; - } - .todo-header h3 { - margin: 0; - color: #212529; - font-size: 1.1em; - } - .todo-content { - color: #495057; - } - .todo-info { - margin-bottom: 8px; - font-size: 0.9em; - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; - } - .todo-info .label { - color: #6c757d; - } - .todo-info .value { - color: #212529; - } - .todo-description { - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid #e9ecef; - color: #495057; - font-size: 0.95em; - } - .description-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - } - .description-actions { - display: flex; - gap: 4px; - } - .description-content { - white-space: pre-wrap; - line-height: 1.5; - max-height: 100px; - overflow: hidden; - transition: max-height 0.3s ease-out; - position: relative; - text-indent: 0; /* 移除缩进 */ - } - .description-content.expanded { - max-height: 2000px; - } - .description-content:not(.expanded) { - max-height: 1.5em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-indent: 0; /* 移除缩进 */ - } - .description-content::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 40px; - background: linear-gradient(transparent, white); - pointer-events: none; - opacity: 1; - transition: opacity 0.3s ease-out; - } - .description-content.expanded::after { - opacity: 0; - } - .toggle-button { - background: none; - border: none; - cursor: pointer; - padding: 4px; - border-radius: 4px; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - } - .toggle-button:hover { - background-color: rgba(121, 134, 231, 0.1); - } - .toggle-icon { - transition: transform 0.3s ease; - width: 16px; - height: 16px; - display: inline-block; - } - .toggle-button.expanded .toggle-icon { - transform: rotate(180deg); - } - .status { - display: inline-block; - padding: 4px 8px; - border-radius: 4px; - font-size: 0.9em; - } - .status.completed { - background: #d4edda; - color: #155724; - } - .status.pending { - background: #fff3cd; - color: #856404; - } - .error-message { - color: #721c24; - padding: 10px; - background: #f8d7da; - border-radius: 4px; - margin: 10px; - } - .selected { - background: #cfe2ff !important; - } - .executor-link { - color: #007bff; - text-decoration: none; - cursor: pointer; - } - .executor-link:hover { - text-decoration: underline; - } - .schedule-link { - color: #007bff; - text-decoration: none; - cursor: pointer; - } - .schedule-link:hover { - text-decoration: underline; - } - .add-todo-button { - margin-bottom: 15px; - padding: 8px 16px; - background-color: #7986E7; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: background-color 0.2s; - width: 100%; - } - .add-todo-button:hover { - background-color: #6875D6; - } - .add-todo-button:active { - background-color: #5A67D8; - } - .todo-actions { - display: flex; - align-items: center; - gap: 8px; - } - .edit-button, .delete-button, .complete-button { - background: none; - border: none; - cursor: pointer; - padding: 4px; - border-radius: 4px; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - } - .action-icon { - width: 16px; - height: 16px; - object-fit: contain; - } - .edit-button:hover { - background-color: rgba(121, 134, 231, 0.1); - } - .delete-button:hover { - background-color: rgba(220, 53, 69, 0.1); - } - .complete-button:hover { - background-color: rgba(40, 167, 69, 0.1); - } - `; - - const modal = document.createElement('div'); - modal.className = 'modal'; - - // 根据类型创建不同的模态框内容 - if (type === 'executor') { - modal.innerHTML = ` - - `; - } else if (type === 'schedule') { - modal.innerHTML = ` - - `; - } else if (type === 'new-todo') { - modal.innerHTML = ` - - `; - } else if (type === 'edit-title') { - modal.innerHTML = ` - - `; - } else if (type === 'edit-description') { - modal.innerHTML = ` - - `; + // 过滤待办事项 + filterTodos() { + if (!this.currentUser) { + console.log('未获取到当前用户信息,无法进行筛选'); + return []; } - // 添加关闭按钮事件 - const closeButton = modal.querySelector('.close-button'); - closeButton.addEventListener('click', () => { - modal.remove(); - style.remove(); + // 首先根据项目/子项目筛选 + let filteredTodos = this.todos; + if (this.selectedProject) { + filteredTodos = filteredTodos.filter(todo => { + const matchesProject = todo.project === this.selectedProject; + if (!this.selectedSubproject) { + return matchesProject; + } + return matchesProject && todo.subproject === this.selectedSubproject; + }); + } + + // 根据复选框状态筛选 + if (!this.showCompleted) { + filteredTodos = filteredTodos.filter(todo => !todo.completed); + } + + // 根据用户等级进行筛选 + const userLevel = this.currentUser.level || 0; + + // 等级4可以看到所有待办事项 + if (userLevel === 4) { + return filteredTodos; + } + + // 等级3不能看到创建人和执行人都是等级4的待办事项 + if (userLevel === 3) { + return filteredTodos.filter(todo => { + const creator = this.users.find(u => u.username === todo.adduser); + const executor = this.users.find(u => u.username === todo.exeuser); + + // 如果找不到用户信息,默认显示 + if (!creator || !executor) return true; + + // 只要创建人或执行人不是等级4,就显示 + return !(creator.level === 4 && executor.level === 4); + }); + } + + // 等级2或更低只能看到创建人或执行人是自己的待办事项 + return filteredTodos.filter(todo => { + return todo.adduser === this.currentUser.username || + todo.exeuser === this.currentUser.username; }); - - // 添加取消按钮事件 - const cancelButton = modal.querySelector('.cancel-button'); - cancelButton.addEventListener('click', () => { - modal.remove(); - style.remove(); - }); - - // 将样式添加到文档头部 - document.head.appendChild(style); - - return { modal, style }; } render() { @@ -1276,10 +400,10 @@ class TodoComponent extends HTMLElement { style.textContent = ` .container { display: flex; - height: calc(100vh - 120px); /* 增加边距,减小高度 */ + height: calc(100vh - 120px); font-family: Arial, sans-serif; margin: 10px; - overflow: hidden; /* 防止出现双滚动条 */ + overflow: hidden; } .project-tree { width: 250px; @@ -1290,13 +414,13 @@ class TodoComponent extends HTMLElement { flex-direction: column; position: relative; height: 100%; - box-sizing: border-box; /* 确保padding不会增加总高度 */ + box-sizing: border-box; } .tree-content { flex: 1; overflow-y: auto; - margin-bottom: 100px; /* 为底部固定区域留出空间 */ - padding-right: 5px; /* 为滚动条留出空间 */ + margin-bottom: 100px; + padding-right: 5px; } .todo-container { flex: 1; @@ -1315,7 +439,6 @@ class TodoComponent extends HTMLElement { border-top: 1px solid #e9ecef; box-sizing: border-box; } - /* 自定义滚动条样式 */ .tree-content::-webkit-scrollbar, .todo-container::-webkit-scrollbar { width: 6px; @@ -1481,7 +604,7 @@ class TodoComponent extends HTMLElement { overflow: hidden; transition: max-height 0.3s ease-out; position: relative; - text-indent: 0; /* 移除缩进 */ + text-indent: 0; } .description-content.expanded { max-height: 2000px; @@ -1491,7 +614,7 @@ class TodoComponent extends HTMLElement { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - text-indent: 0; /* 移除缩进 */ + text-indent: 0; } .description-content::after { content: ''; @@ -1627,100 +750,49 @@ class TodoComponent extends HTMLElement { container.className = 'container'; // 构建并渲染项目树 - const tree = this.buildProjectTree(); - const treeContainer = this.renderProjectTree(tree); + const tree = TodoTree.buildProjectTree(this.todos); + const treeContainer = TodoTree.renderProjectTree(tree, { + expandedProjects: this.expandedProjects, + selectedProject: this.selectedProject, + selectedSubproject: this.selectedSubproject, + showCompleted: this.showCompleted, + onProjectSelect: (project) => { + this.selectedProject = project; + this.selectedSubproject = null; + this.render(); + }, + onSubprojectSelect: (project, subproject) => { + this.selectedProject = project; + this.selectedSubproject = subproject; + this.render(); + }, + onExpandToggle: (project) => { + if (this.expandedProjects.has(project)) { + this.expandedProjects.delete(project); + } else { + this.expandedProjects.add(project); + } + this.render(); + }, + onShowCompletedChange: (show) => { + this.showCompleted = show; + this.render(); + }, + onShowAll: () => { + this.selectedProject = null; + this.selectedSubproject = null; + this.render(); + }, + onAddTodo: () => this.showNewTodoModal() + }); - // 创建待办事项列表容器 - const todosContainer = document.createElement('div'); - todosContainer.className = 'todo-container'; - // 渲染过滤后的待办事项 const filteredTodos = this.filterTodos(); - filteredTodos.forEach(todo => { - const todoElement = document.createElement('div'); - todoElement.className = 'todo-item'; - - // 格式化日期 - const createdDate = new Date(todo.created_at).toLocaleString('zh-CN'); - const scheDate = new Date(todo.sche_time).toLocaleString('zh-CN'); - const completeDate = todo.complete_time ? - new Date(todo.complete_time).toLocaleString('zh-CN') : ''; - - todoElement.innerHTML = ` -
-
- -
-

${todo.project} - ${todo.subproject} - ${todo.title}

-
- - - - ${todo.completed ? '已完成' : '进行中'} - -
-
-
-
- 执行人: - ${todo.exeuser || '未分配'} - - 创建人: - ${todo.adduser} -
-
- 计划时间: - ${scheDate} - 创建时间: - ${createdDate} -
- ${todo.completed ? ` -
- 完成时间: - ${completeDate} -
- ` : ''} -
-
- 详细描述: -
- - -
-
-
${todo.text || '无详细描述'}
-
-
- `; - - // 添加执行人链接点击事件 - const executorLink = todoElement.querySelector('.executor-link'); - executorLink.addEventListener('click', (e) => { - e.preventDefault(); - this.showEditExecutorModal(todo); - }); - - // 添加计划时间链接点击事件 - const scheduleLink = todoElement.querySelector('.schedule-link'); - scheduleLink.addEventListener('click', (e) => { - e.preventDefault(); - this.showEditScheduleModal(todo); - }); - - // 添加编辑按钮事件 - const editButton = todoElement.querySelector('.edit-button'); - editButton.addEventListener('click', () => { - const { modal, style } = this.createModal('edit-title'); + const todosContainer = TodoList.renderTodoList(filteredTodos, { + onEditExecutor: (todo) => this.showEditExecutorModal(todo), + onEditSchedule: (todo) => this.showEditScheduleModal(todo), + onEditTitle: (todo) => { + const { modal, style } = TodoModal.createModal('edit-title'); const titleInput = modal.querySelector('#title'); titleInput.value = todo.title; @@ -1733,21 +805,11 @@ class TodoComponent extends HTMLElement { } try { - const response = await fetch(`/api/todos/${todo.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - ...todo, - title: newTitle - }) + await TodoService.updateTodo(todo.id, { + ...todo, + title: newTitle }); - if (!response.ok) { - throw new Error('更新标题失败'); - } - await this.fetchTodos(); this.render(); modal.remove(); @@ -1759,12 +821,9 @@ class TodoComponent extends HTMLElement { }); document.body.appendChild(modal); - }); - - // 添加描述编辑按钮事件 - const descriptionEditButton = todoElement.querySelector('.todo-description .edit-button'); - descriptionEditButton.addEventListener('click', () => { - const { modal, style } = this.createModal('edit-description'); + }, + onEditDescription: (todo) => { + const { modal, style } = TodoModal.createModal('edit-description'); const textInput = modal.querySelector('#text'); textInput.value = todo.text || ''; @@ -1773,21 +832,11 @@ class TodoComponent extends HTMLElement { const newText = textInput.value; try { - const response = await fetch(`/api/todos/${todo.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - ...todo, - text: newText - }) + await TodoService.updateTodo(todo.id, { + ...todo, + text: newText }); - if (!response.ok) { - throw new Error('更新描述失败'); - } - await this.fetchTodos(); this.render(); modal.remove(); @@ -1799,21 +848,11 @@ class TodoComponent extends HTMLElement { }); document.body.appendChild(modal); - }); - - // 添加删除按钮事件 - const deleteButton = todoElement.querySelector('.delete-button'); - deleteButton.addEventListener('click', async () => { + }, + onDelete: async (todo) => { if (confirm('确定要删除这条待办事项吗?')) { try { - const response = await fetch(`/api/todos/${todo.id}`, { - method: 'DELETE' - }); - - if (!response.ok) { - throw new Error('删除待办事项失败'); - } - + await TodoService.deleteTodo(todo.id); await this.fetchTodos(); this.render(); } catch (error) { @@ -1821,30 +860,17 @@ class TodoComponent extends HTMLElement { this.showError('删除待办事项失败,请稍后重试'); } } - }); - - // 添加完成/取消完成按钮事件 - const completeButton = todoElement.querySelector('.complete-button'); - completeButton.addEventListener('click', async () => { + }, + onComplete: async (todo) => { const action = todo.completed ? '取消完成' : '标记完成'; if (confirm(`确定要${action}这条待办事项吗?`)) { try { - const response = await fetch(`/api/todos/${todo.id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - ...todo, - completed: !todo.completed, - complete_time: !todo.completed ? new Date().toISOString() : null - }) + await TodoService.updateTodo(todo.id, { + ...todo, + completed: !todo.completed, + complete_time: !todo.completed ? new Date().toISOString() : null }); - if (!response.ok) { - throw new Error(`${action}待办事项失败`); - } - await this.fetchTodos(); this.render(); } catch (error) { @@ -1852,34 +878,18 @@ class TodoComponent extends HTMLElement { this.showError(`${action}待办事项失败,请稍后重试`); } } - }); - - // 添加展开/收起按钮事件 - const toggleButton = todoElement.querySelector('.toggle-button'); - const descriptionContent = todoElement.querySelector('.description-content'); - const toggleIcon = toggleButton.querySelector('.toggle-icon'); - - // 移除可能存在的旧事件监听器 - const newToggleButton = toggleButton.cloneNode(true); - toggleButton.parentNode.replaceChild(newToggleButton, toggleButton); - - // 添加新的事件监听器 - newToggleButton.addEventListener('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - + }, + onToggleDescription: (descriptionContent, toggleButton) => { const isExpanded = descriptionContent.classList.contains('expanded'); if (isExpanded) { descriptionContent.classList.remove('expanded'); - newToggleButton.classList.remove('expanded'); + toggleButton.classList.remove('expanded'); } else { descriptionContent.classList.add('expanded'); - newToggleButton.classList.add('expanded'); + toggleButton.classList.add('expanded'); } - }); - - todosContainer.appendChild(todoElement); + } }); container.appendChild(treeContainer); diff --git a/XNSimHtml/components/todo/todo-list.js b/XNSimHtml/components/todo/todo-list.js new file mode 100644 index 0000000..3602796 --- /dev/null +++ b/XNSimHtml/components/todo/todo-list.js @@ -0,0 +1,142 @@ +class TodoList { + static renderTodoList(todos, { + onEditExecutor, + onEditSchedule, + onEditTitle, + onEditDescription, + onDelete, + onComplete, + onToggleDescription + }) { + const todosContainer = document.createElement('div'); + todosContainer.className = 'todo-container'; + + todos.forEach(todo => { + const todoElement = document.createElement('div'); + todoElement.className = 'todo-item'; + + // 格式化日期 + const createdDate = new Date(todo.created_at).toLocaleString('zh-CN'); + const scheDate = new Date(todo.sche_time).toLocaleString('zh-CN'); + const completeDate = todo.complete_time ? + new Date(todo.complete_time).toLocaleString('zh-CN') : ''; + + todoElement.innerHTML = ` +
+
+ +
+

${todo.project} - ${todo.subproject} - ${todo.title}

+
+ + + + ${todo.completed ? '已完成' : '进行中'} + +
+
+
+
+ 执行人: + ${todo.exeuser || '未分配'} + + 创建人: + ${todo.adduser} +
+
+ 计划时间: + ${scheDate} + 创建时间: + ${createdDate} +
+ ${todo.completed ? ` +
+ 完成时间: + ${completeDate} +
+ ` : ''} +
+
+ 详细描述: +
+ + +
+
+
${todo.text || '无详细描述'}
+
+
+ `; + + // 添加执行人链接点击事件 + const executorLink = todoElement.querySelector('.executor-link'); + executorLink.addEventListener('click', (e) => { + e.preventDefault(); + onEditExecutor(todo); + }); + + // 添加计划时间链接点击事件 + const scheduleLink = todoElement.querySelector('.schedule-link'); + scheduleLink.addEventListener('click', (e) => { + e.preventDefault(); + onEditSchedule(todo); + }); + + // 添加编辑按钮事件 + const editButton = todoElement.querySelector('.edit-button'); + editButton.addEventListener('click', () => { + onEditTitle(todo); + }); + + // 添加描述编辑按钮事件 + const descriptionEditButton = todoElement.querySelector('.todo-description .edit-button'); + descriptionEditButton.addEventListener('click', () => { + onEditDescription(todo); + }); + + // 添加删除按钮事件 + const deleteButton = todoElement.querySelector('.delete-button'); + deleteButton.addEventListener('click', () => { + onDelete(todo); + }); + + // 添加完成/取消完成按钮事件 + const completeButton = todoElement.querySelector('.complete-button'); + completeButton.addEventListener('click', () => { + onComplete(todo); + }); + + // 添加展开/收起按钮事件 + const toggleButton = todoElement.querySelector('.toggle-button'); + const descriptionContent = todoElement.querySelector('.description-content'); + const toggleIcon = toggleButton.querySelector('.toggle-icon'); + + // 移除可能存在的旧事件监听器 + const newToggleButton = toggleButton.cloneNode(true); + toggleButton.parentNode.replaceChild(newToggleButton, toggleButton); + + // 添加新的事件监听器 + newToggleButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + onToggleDescription(descriptionContent, newToggleButton); + }); + + todosContainer.appendChild(todoElement); + }); + + return todosContainer; + } +} + +export default TodoList; diff --git a/XNSimHtml/components/todo/todo-modal.js b/XNSimHtml/components/todo/todo-modal.js new file mode 100644 index 0000000..d57d377 --- /dev/null +++ b/XNSimHtml/components/todo/todo-modal.js @@ -0,0 +1,414 @@ +class TodoModal { + static createModal(type) { + // 创建样式 + const style = document.createElement('style'); + style.textContent = ` + .modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + } + .modal-content { + background-color: white; + border-radius: 8px; + width: 400px; + max-width: 90%; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + position: relative; + margin: auto; + } + .modal-header { + padding: 15px; + border-bottom: 1px solid #e9ecef; + display: flex; + justify-content: space-between; + align-items: center; + } + .modal-header h3 { + margin: 0; + color: #212529; + } + .close-button { + background: none; + border: none; + font-size: 1.5em; + color: #6c757d; + cursor: pointer; + padding: 0; + line-height: 1; + } + .modal-body { + padding: 15px; + } + .modal-footer { + padding: 15px; + border-top: 1px solid #e9ecef; + display: flex; + justify-content: flex-end; + gap: 10px; + } + .form-group { + margin-bottom: 15px; + } + .form-group label { + display: block; + margin-bottom: 5px; + color: #495057; + } + .form-group input[type="text"], + .form-group select { + width: 100%; + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + margin-bottom: 5px; + background-color: white; + } + .form-group input[type="text"]:focus, + .form-group select:focus { + border-color: #7986E7; + outline: none; + box-shadow: 0 0 0 2px rgba(121, 134, 231, 0.2); + } + .form-group select:disabled { + background-color: #f8f9fa; + cursor: not-allowed; + } + .form-group select[contenteditable="true"] { + cursor: text; + } + .form-group input[list] { + width: 100%; + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + margin-bottom: 5px; + background-color: white; + } + .form-group input[list]:focus { + border-color: #7986E7; + outline: none; + box-shadow: 0 0 0 2px rgba(121, 134, 231, 0.2); + } + .form-group datalist { + width: 100%; + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + background-color: white; + } + .form-group input[list]::-webkit-calendar-picker-indicator { + display: none; + } + .form-group input[list]::-webkit-list-button { + display: none; + } + .form-group input[list]::-webkit-datetime-edit { + padding: 0; + } + .form-group input[list]::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + .form-group input[list]::-webkit-datetime-edit-text { + padding: 0 2px; + } + .form-group input[list]::-webkit-datetime-edit-hour-field, + .form-group input[list]::-webkit-datetime-edit-minute-field, + .form-group input[list]::-webkit-datetime-edit-second-field, + .form-group input[list]::-webkit-datetime-edit-ampm-field { + padding: 0 2px; + } + .form-group input[list] + datalist { + position: absolute; + background: white; + border: 1px solid #ddd; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + max-height: 200px; + overflow-y: auto; + z-index: 1000; + } + .form-group input[list] + datalist option { + padding: 8px 10px; + cursor: pointer; + font-size: 14px; + color: #333; + } + .form-group input[list] + datalist option:hover { + background-color: #f5f5f5; + } + .save-button, .cancel-button { + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + } + .save-button { + background-color: #7986E7; + color: white; + } + .save-button:hover { + background-color: #6875D6; + } + .cancel-button { + background-color: #6c757d; + color: white; + } + .cancel-button:hover { + background-color: #5a6268; + } + input[type="datetime-local"] { + font-family: inherit; + color: #333; + } + input[type="datetime-local"]::-webkit-calendar-picker-indicator { + cursor: pointer; + padding: 4px; + margin-right: 4px; + opacity: 0.6; + } + input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover { + opacity: 1; + } + input[type="datetime-local"]::-webkit-datetime-edit { + padding: 0; + } + input[type="datetime-local"]::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + input[type="datetime-local"]::-webkit-datetime-edit-text { + padding: 0 2px; + } + input[type="datetime-local"]::-webkit-datetime-edit-hour-field, + input[type="datetime-local"]::-webkit-datetime-edit-minute-field, + input[type="datetime-local"]::-webkit-datetime-edit-second-field, + input[type="datetime-local"]::-webkit-datetime-edit-ampm-field { + padding: 0 2px; + } + .form-group textarea { + width: 100%; + min-height: 100px; + padding: 8px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + resize: vertical; + } + .form-group textarea:focus { + border-color: #7986E7; + outline: none; + box-shadow: 0 0 0 2px rgba(121, 134, 231, 0.2); + } + .custom-select { + position: relative; + width: 100%; + } + .select-options { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #7986E7; + border-radius: 4px; + box-shadow: 0 4px 8px rgba(121, 134, 231, 0.2); + max-height: 200px; + overflow-y: auto; + z-index: 1000; + margin-top: 2px; + } + .select-option { + padding: 8px 10px; + cursor: pointer; + font-size: 14px; + color: #333; + transition: all 0.2s ease; + } + .select-option:hover { + background-color: #7986E7; + color: white; + } + .select-option:active { + background-color: #6875D6; + } + .select-options::-webkit-scrollbar { + width: 6px; + } + .select-options::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; + } + .select-options::-webkit-scrollbar-thumb { + background: #7986E7; + border-radius: 3px; + } + .select-options::-webkit-scrollbar-thumb:hover { + background: #6875D6; + } + `; + + const modal = document.createElement('div'); + modal.className = 'modal'; + + // 根据类型创建不同的模态框内容 + if (type === 'executor') { + modal.innerHTML = ` + + `; + } else if (type === 'schedule') { + modal.innerHTML = ` + + `; + } else if (type === 'new-todo') { + modal.innerHTML = ` + + `; + } else if (type === 'edit-title') { + modal.innerHTML = ` + + `; + } else if (type === 'edit-description') { + modal.innerHTML = ` + + `; + } + + // 添加关闭按钮事件 + const closeButton = modal.querySelector('.close-button'); + closeButton.addEventListener('click', () => { + modal.remove(); + style.remove(); + }); + + // 添加取消按钮事件 + const cancelButton = modal.querySelector('.cancel-button'); + cancelButton.addEventListener('click', () => { + modal.remove(); + style.remove(); + }); + + // 将样式添加到文档头部 + document.head.appendChild(style); + + return { modal, style }; + } +} + +export default TodoModal; diff --git a/XNSimHtml/components/todo/todo-service.js b/XNSimHtml/components/todo/todo-service.js new file mode 100644 index 0000000..3198809 --- /dev/null +++ b/XNSimHtml/components/todo/todo-service.js @@ -0,0 +1,61 @@ +class TodoService { + static async fetchTodos() { + const response = await fetch('/api/todos'); + if (!response.ok) { + throw new Error('获取待办事项失败'); + } + return await response.json(); + } + + static async fetchUsers() { + const response = await fetch('/api/users'); + if (!response.ok) { + throw new Error('获取用户数据失败'); + } + const data = await response.json(); + return data.users || []; + } + + static async updateTodo(todoId, todoData) { + const response = await fetch(`/api/todos/${todoId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(todoData) + }); + + if (!response.ok) { + throw new Error('更新待办事项失败'); + } + return await response.json(); + } + + static async createTodo(todoData) { + const response = await fetch('/api/todos', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(todoData) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || '创建待办失败'); + } + return await response.json(); + } + + static async deleteTodo(todoId) { + const response = await fetch(`/api/todos/${todoId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + throw new Error('删除待办事项失败'); + } + } +} + +export default TodoService; diff --git a/XNSimHtml/components/todo/todo-tree.js b/XNSimHtml/components/todo/todo-tree.js new file mode 100644 index 0000000..f087d75 --- /dev/null +++ b/XNSimHtml/components/todo/todo-tree.js @@ -0,0 +1,139 @@ +class TodoTree { + static buildProjectTree(todos) { + const tree = {}; + todos.forEach(todo => { + const project = todo.project || '其它'; + const subproject = todo.subproject || '其它'; + + if (!tree[project]) { + tree[project] = { + name: project, + subprojects: {} + }; + } + if (!tree[project].subprojects[subproject]) { + tree[project].subprojects[subproject] = { + name: subproject, + todos: [] + }; + } + tree[project].subprojects[subproject].todos.push(todo); + }); + return tree; + } + + static renderProjectTree(tree, { + expandedProjects, + selectedProject, + selectedSubproject, + showCompleted, + onProjectSelect, + onSubprojectSelect, + onExpandToggle, + onShowCompletedChange, + onShowAll, + onAddTodo + }) { + const treeContainer = document.createElement('div'); + treeContainer.className = 'project-tree'; + + // 创建树内容容器 + const treeContent = document.createElement('div'); + treeContent.className = 'tree-content'; + + // 添加新增待办按钮 + const addTodoButton = document.createElement('button'); + addTodoButton.className = 'add-todo-button'; + addTodoButton.textContent = '新增待办'; + addTodoButton.addEventListener('click', onAddTodo); + treeContent.appendChild(addTodoButton); + + Object.values(tree).forEach(project => { + const projectNode = document.createElement('div'); + projectNode.className = 'project-node'; + + const projectHeader = document.createElement('div'); + projectHeader.className = 'project-header'; + projectHeader.innerHTML = ` +
+ + ${project.name} + (${Object.values(project.subprojects).reduce((sum, sub) => + sum + sub.todos.length, 0)}) +
+ `; + + projectHeader.addEventListener('click', () => { + onProjectSelect(project.name); + }); + + const expandButton = projectHeader.querySelector('.expand-icon'); + expandButton.addEventListener('click', (e) => { + e.stopPropagation(); + onExpandToggle(project.name); + }); + + const subprojectsContainer = document.createElement('div'); + subprojectsContainer.className = 'subprojects-container'; + subprojectsContainer.style.display = expandedProjects.has(project.name) ? 'block' : 'none'; + + Object.values(project.subprojects).forEach(subproject => { + const subprojectNode = document.createElement('div'); + subprojectNode.className = 'subproject-node'; + if (selectedProject === project.name && selectedSubproject === subproject.name) { + subprojectNode.classList.add('selected'); + } + subprojectNode.innerHTML = ` + ${subproject.name} + (${subproject.todos.length}) + `; + + subprojectNode.addEventListener('click', (e) => { + e.stopPropagation(); + onSubprojectSelect(project.name, subproject.name); + }); + + subprojectsContainer.appendChild(subprojectNode); + }); + + projectNode.appendChild(projectHeader); + projectNode.appendChild(subprojectsContainer); + treeContent.appendChild(projectNode); + }); + + // 创建底部控制区域 + const bottomControls = document.createElement('div'); + bottomControls.className = 'bottom-controls'; + + // 添加显示已完成复选框 + const showCompletedContainer = document.createElement('div'); + showCompletedContainer.className = 'show-completed-container'; + showCompletedContainer.innerHTML = ` + + + `; + + // 添加复选框事件监听 + const checkbox = showCompletedContainer.querySelector('input'); + checkbox.addEventListener('change', (e) => { + onShowCompletedChange(e.target.checked); + }); + + bottomControls.appendChild(showCompletedContainer); + + // 添加显示全部按钮 + const showAllButton = document.createElement('button'); + showAllButton.className = 'show-all-button'; + showAllButton.textContent = '显示全部'; + showAllButton.addEventListener('click', onShowAll); + bottomControls.appendChild(showAllButton); + + // 将树内容和底部控制区域添加到树容器 + treeContainer.appendChild(treeContent); + treeContainer.appendChild(bottomControls); + + return treeContainer; + } +} + +export default TodoTree;