class TodoComponent extends HTMLElement { static { // 添加静态初始化标志 this.isInitializing = false; } constructor() { super(); this.attachShadow({ mode: 'open' }); this.todos = []; this.selectedProject = null; this.selectedSubproject = null; this.expandedProjects = new Set(); this.users = []; this.currentUser = null; this.editingTodo = null; // 当前正在编辑的待办事项 this.isInitialized = false; // 添加初始化标志 this.showCompleted = true; // 添加显示已完成的状态变量 } // 初始化组件 async initialize() { // 检查是否正在初始化 if (TodoComponent.isInitializing) { return; } if (this.isInitialized) { return; } TodoComponent.isInitializing = true; try { await Promise.all([ this.fetchTodos(), this.fetchUsers(), this.getCurrentUser() ]); this.isInitialized = true; this.render(); } catch (error) { console.error('初始化组件时发生错误:', error); this.showError('初始化失败,请刷新页面重试'); } finally { TodoComponent.isInitializing = false; } } async fetchTodos() { try { const response = await fetch('/api/todos'); if (!response.ok) { throw new Error('获取待办事项失败'); } this.todos = await response.json(); } catch (error) { console.error('获取待办事项时发生错误:', error); this.showError('获取待办事项失败,请稍后重试'); } } async fetchUsers() { try { const response = await fetch('/api/users'); if (!response.ok) { throw new Error('获取用户数据失败'); } const data = await response.json(); this.users = data.users || []; } catch (error) { console.error('获取用户数据时发生错误:', error); this.showError('获取用户数据失败,请稍后重试'); } } getCurrentUser() { try { const userStr = localStorage.getItem('userInfo'); if (userStr) { this.currentUser = JSON.parse(userStr); } else { console.log('未找到登录用户信息'); } } catch (error) { console.error('获取当前用户信息时发生错误:', error); this.showError('获取用户信息失败'); } } showError(message) { const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.textContent = message; 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 currentUserLevel = this.currentUser.access_level || 0; // 填充用户下拉列表 const executorSelect = modal.querySelector('#executor'); // 筛选符合条件的用户 const availableUsers = this.users.filter(user => user && user.access_level > 0 && user.access_level <= currentUserLevel ); // 添加用户选项 availableUsers.forEach(user => { const option = document.createElement('option'); option.value = user.username; option.textContent = `${user.username} (${user.full_name})`; if (user.username === todo.exeuser) { option.selected = true; } executorSelect.appendChild(option); }); // 添加保存按钮事件 const saveButton = modal.querySelector('.save-button'); saveButton.addEventListener('click', async () => { 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 }) }); if (!response.ok) { throw new Error('更新执行人失败'); } // 更新本地数据 this.editingTodo.exeuser = executor; 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 scheduleInput = modal.querySelector('#schedule'); const now = new Date(); scheduleInput.value = now.toISOString().slice(0, 16); // 获取当前用户等级 const currentUserLevel = this.currentUser.access_level || 0; // 填充用户下拉列表 const executorSelect = modal.querySelector('#executor'); // 筛选符合条件的用户 const availableUsers = this.users.filter(user => user && user.access_level > 0 && user.access_level <= currentUserLevel ); // 添加用户选项 availableUsers.forEach(user => { const option = document.createElement('option'); option.value = user.username; option.textContent = `${user.username} (${user.full_name})`; executorSelect.appendChild(option); }); // 获取所有项目和子项目 const projectTree = this.buildProjectTree(); const projectInput = modal.querySelector('#project'); const subprojectInput = modal.querySelector('#subproject'); const projectOptions = modal.querySelector('#project-options'); const subprojectOptions = modal.querySelector('#subproject-options'); // 填充项目选项 Object.keys(projectTree).forEach(project => { const option = document.createElement('div'); option.className = 'select-option'; option.textContent = project; option.addEventListener('click', () => { projectInput.value = project; projectOptions.style.display = 'none'; // 更新子项目选项 updateSubprojectOptions(project); }); projectOptions.appendChild(option); }); // 更新子项目选项的函数 const updateSubprojectOptions = (selectedProject) => { subprojectOptions.innerHTML = ''; const subprojects = projectTree[selectedProject]?.subprojects || {}; Object.keys(subprojects).forEach(subproject => { const option = document.createElement('div'); option.className = 'select-option'; option.textContent = subproject; option.addEventListener('click', () => { subprojectInput.value = subproject; subprojectOptions.style.display = 'none'; }); subprojectOptions.appendChild(option); }); }; // 项目输入框事件处理 projectInput.addEventListener('focus', () => { projectOptions.style.display = 'block'; subprojectOptions.style.display = 'none'; }); projectInput.addEventListener('input', () => { const value = projectInput.value.toLowerCase(); const options = projectOptions.querySelectorAll('.select-option'); options.forEach(option => { const text = option.textContent.toLowerCase(); option.style.display = text.includes(value) ? 'block' : 'none'; }); projectOptions.style.display = 'block'; }); // 子项目输入框事件处理 subprojectInput.addEventListener('focus', () => { if (projectInput.value) { subprojectOptions.style.display = 'block'; projectOptions.style.display = 'none'; } }); subprojectInput.addEventListener('input', () => { if (projectInput.value) { const value = subprojectInput.value.toLowerCase(); const options = subprojectOptions.querySelectorAll('.select-option'); options.forEach(option => { const text = option.textContent.toLowerCase(); option.style.display = text.includes(value) ? 'block' : 'none'; }); subprojectOptions.style.display = 'block'; } }); // 点击外部关闭下拉列表 document.addEventListener('click', (e) => { if (!e.target.closest('.custom-select')) { projectOptions.style.display = 'none'; subprojectOptions.style.display = 'none'; } }); // 添加保存按钮事件 const saveButton = modal.querySelector('.save-button'); saveButton.addEventListener('click', async () => { const project = projectInput.value; const subproject = subprojectInput.value; const title = modal.querySelector('#title').value; const text = modal.querySelector('#text').value; const scheduleTime = modal.querySelector('#schedule').value; const executor = modal.querySelector('#executor').value; if (!project || !subproject || !scheduleTime || !executor || !title) { this.showError('请填写必填项'); return; } 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' // 添加初始状态 }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || '创建待办失败'); } // 重新加载待办列表并渲染 await this.fetchTodos(); this.render(); modal.remove(); style.remove(); } catch (error) { console.error('创建待办时发生错误:', error); this.showError(error.message || '创建待办失败,请稍后重试'); } }); 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; } `; 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 }; } render() { if (!this.isInitialized) { return; } // 清空现有内容 this.shadowRoot.innerHTML = ''; // 添加样式 const style = document.createElement('style'); style.textContent = ` .container { display: flex; height: calc(100vh - 120px); /* 增加边距,减小高度 */ font-family: Arial, sans-serif; margin: 10px; overflow: hidden; /* 防止出现双滚动条 */ } .project-tree { width: 250px; background: #f8f9fa; border-right: 1px solid #e9ecef; padding: 15px; display: flex; flex-direction: column; position: relative; height: 100%; box-sizing: border-box; /* 确保padding不会增加总高度 */ } .tree-content { flex: 1; overflow-y: auto; margin-bottom: 100px; /* 为底部固定区域留出空间 */ padding-right: 5px; /* 为滚动条留出空间 */ } .todo-container { flex: 1; padding: 20px; overflow-y: auto; height: 100%; box-sizing: border-box; } .bottom-controls { position: absolute; bottom: 0; left: 0; right: 0; padding: 15px; background: #f8f9fa; border-top: 1px solid #e9ecef; box-sizing: border-box; } /* 自定义滚动条样式 */ .tree-content::-webkit-scrollbar, .todo-container::-webkit-scrollbar { width: 6px; } .tree-content::-webkit-scrollbar-track, .todo-container::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 3px; } .tree-content::-webkit-scrollbar-thumb, .todo-container::-webkit-scrollbar-thumb { background: #888; border-radius: 3px; } .tree-content::-webkit-scrollbar-thumb:hover, .todo-container::-webkit-scrollbar-thumb:hover { background: #555; } .show-completed-container { margin-bottom: 10px; padding: 8px; display: flex; align-items: center; gap: 8px; background: #fff; border-radius: 4px; border: 1px solid #e9ecef; } .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); } `; this.shadowRoot.appendChild(style); // 创建主容器 const container = document.createElement('div'); container.className = 'container'; // 构建并渲染项目树 const tree = this.buildProjectTree(); const treeContainer = this.renderProjectTree(tree); // 创建待办事项列表容器 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 titleInput = modal.querySelector('#title'); titleInput.value = todo.title; const saveButton = modal.querySelector('.save-button'); saveButton.addEventListener('click', async () => { const newTitle = titleInput.value; if (!newTitle) { this.showError('标题不能为空'); return; } try { const response = await fetch(`/api/todos/${todo.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...todo, title: newTitle }) }); if (!response.ok) { throw new Error('更新标题失败'); } await this.fetchTodos(); this.render(); modal.remove(); style.remove(); } catch (error) { console.error('更新标题时发生错误:', error); this.showError('更新标题失败,请稍后重试'); } }); document.body.appendChild(modal); }); // 添加描述编辑按钮事件 const descriptionEditButton = todoElement.querySelector('.todo-description .edit-button'); descriptionEditButton.addEventListener('click', () => { const { modal, style } = this.createModal('edit-description'); const textInput = modal.querySelector('#text'); textInput.value = todo.text || ''; const saveButton = modal.querySelector('.save-button'); saveButton.addEventListener('click', async () => { 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 }) }); if (!response.ok) { throw new Error('更新描述失败'); } await this.fetchTodos(); this.render(); modal.remove(); style.remove(); } catch (error) { console.error('更新描述时发生错误:', error); this.showError('更新描述失败,请稍后重试'); } }); document.body.appendChild(modal); }); // 添加删除按钮事件 const deleteButton = todoElement.querySelector('.delete-button'); deleteButton.addEventListener('click', async () => { if (confirm('确定要删除这条待办事项吗?')) { try { const response = await fetch(`/api/todos/${todo.id}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('删除待办事项失败'); } await this.fetchTodos(); this.render(); } catch (error) { console.error('删除待办事项时发生错误:', error); this.showError('删除待办事项失败,请稍后重试'); } } }); // 添加完成/取消完成按钮事件 const completeButton = todoElement.querySelector('.complete-button'); completeButton.addEventListener('click', async () => { 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 }) }); if (!response.ok) { throw new Error(`${action}待办事项失败`); } await this.fetchTodos(); this.render(); } catch (error) { console.error(`${action}待办事项时发生错误:`, error); 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(); const isExpanded = descriptionContent.classList.contains('expanded'); if (isExpanded) { descriptionContent.classList.remove('expanded'); newToggleButton.classList.remove('expanded'); } else { descriptionContent.classList.add('expanded'); newToggleButton.classList.add('expanded'); } }); todosContainer.appendChild(todoElement); }); container.appendChild(treeContainer); container.appendChild(todosContainer); this.shadowRoot.appendChild(container); } connectedCallback() { this.initialize(); } } customElements.define('todo-component', TodoComponent);