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 { // 添加静态初始化标志 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; } // 先清除所有数据 this.clearData(); TodoComponent.isInitializing = true; try { // 先获取当前用户信息 await this.getCurrentUser(); // 然后获取其他数据 await Promise.all([ this.fetchTodos(), this.fetchUsers() ]); this.isInitialized = true; this.render(); } catch (error) { console.error('初始化组件时发生错误:', error); this.showError('初始化失败,请刷新页面重试'); } finally { TodoComponent.isInitializing = false; } } async fetchTodos() { try { this.todos = await TodoService.fetchTodos(); } catch (error) { console.error('获取待办事项时发生错误:', error); this.showError('获取待办事项失败,请稍后重试'); } } async fetchUsers() { try { this.users = await TodoService.fetchUsers(); } catch (error) { console.error('获取用户数据时发生错误:', error); this.showError('获取用户数据失败,请稍后重试'); } } async getCurrentUser() { try { const response = await fetch('/api/check-auth', { credentials: 'include' // 重要:允许跨域请求携带cookie }); const result = await response.json(); if (result.success) { this.currentUser = result.user; } else { console.log('未找到登录用户信息'); this.currentUser = null; } } catch (error) { console.error('获取当前用户信息时发生错误:', error); this.showError('获取用户信息失败'); this.currentUser = null; } } showError(message) { // 首先移除之前的错误消息 const existingErrorMessage = this.shadowRoot.querySelector('.error-message'); if (existingErrorMessage) { existingErrorMessage.remove(); } // 创建错误消息元素 const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.textContent = message; // 添加自动消失功能 errorDiv.style.position = 'fixed'; errorDiv.style.top = '20px'; errorDiv.style.left = '50%'; errorDiv.style.transform = 'translateX(-50%)'; errorDiv.style.zIndex = '1000'; errorDiv.style.padding = '10px'; errorDiv.style.backgroundColor = '#f8d7da'; errorDiv.style.color = '#721c24'; errorDiv.style.borderRadius = '4px'; errorDiv.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; // 将错误消息添加到文档主体,而不是shadowRoot document.body.appendChild(errorDiv); // 3秒后自动消失 setTimeout(() => { if (errorDiv.parentNode) { errorDiv.parentNode.removeChild(errorDiv); } }, 3000); } // 显示编辑执行人对话框 showEditExecutorModal(todo) { // 权限检查 const currentUserLevel = this.currentUser.level || 0; if (currentUserLevel <= 2 && todo.adduser !== this.currentUser.username) { this.showError('您没有权限编辑他人创建的待办事项'); return; } if (currentUserLevel === 3 && todo.adduser === 'admin') { this.showError('您没有权限编辑管理员创建的待办事项'); return; } this.editingTodo = todo; const { modal, style } = TodoModal.createModal('executor'); // 获取当前用户等级 const currentUserAccessLevel = this.currentUser.access_level || 0; // 填充用户下拉列表 const executorSelect = modal.querySelector('#executor'); // 筛选符合条件的用户 const availableUsers = this.users.filter(user => user && user.access_level > 0 && user.access_level <= currentUserAccessLevel ); // 添加用户选项 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 { await TodoService.updateTodo(this.editingTodo.id, { ...this.editingTodo, exeuser: executor }); // 更新本地数据 this.editingTodo.exeuser = executor; this.render(); modal.remove(); style.remove(); } catch (error) { console.error('更新执行人时发生错误:', error); this.showError('更新执行人失败,请稍后重试'); } } }); document.body.appendChild(modal); } // 显示编辑计划时间对话框 showEditScheduleModal(todo) { // 权限检查 const currentUserLevel = this.currentUser.level || 0; if (currentUserLevel <= 2 && todo.adduser !== this.currentUser.username) { this.showError('您没有权限编辑他人创建的待办事项'); return; } if (currentUserLevel === 3 && todo.adduser === 'admin') { this.showError('您没有权限编辑管理员创建的待办事项'); return; } this.editingTodo = todo; const { modal, style } = TodoModal.createModal('schedule'); // 设置日期时间选择器的初始值 const scheduleInput = modal.querySelector('#schedule'); const scheduleDate = new Date(todo.sche_time); const year = scheduleDate.getFullYear(); const month = String(scheduleDate.getMonth() + 1).padStart(2, '0'); const day = String(scheduleDate.getDate()).padStart(2, '0'); const hours = String(scheduleDate.getHours()).padStart(2, '0'); const minutes = String(scheduleDate.getMinutes()).padStart(2, '0'); const seconds = String(scheduleDate.getSeconds()).padStart(2, '0'); scheduleInput.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 添加保存按钮事件 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 } = TodoModal.createModal('new-todo'); // 设置计划时间的默认值为当前本地时间 const scheduleInput = modal.querySelector('#schedule'); const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); scheduleInput.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 获取当前用户等级 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 = TodoTree.buildProjectTree(this.todos); 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 { await TodoService.createTodo({ project, subproject, title, text, sche_time: scheduleTime, exeuser: executor, adduser: this.currentUser.username, completed: false, status: 'pending' }); // 重新加载待办列表并渲染 await this.fetchTodos(); this.render(); modal.remove(); style.remove(); } catch (error) { console.error('创建待办时发生错误:', error); this.showError(error.message || '创建待办失败,请稍后重试'); } }); document.body.appendChild(modal); } // 过滤待办事项 filterTodos() { if (!this.currentUser) { console.log('未获取到当前用户信息,无法进行筛选'); return []; } // 首先根据用户等级进行筛选 const userLevel = this.currentUser.level || 0; let filteredTodos = this.todos; // 等级4可以看到所有待办事项 if (userLevel !== 4) { // 等级3不能看到创建人和执行人都是等级4的待办事项 if (userLevel === 3) { filteredTodos = 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); }); } else { // 等级2或更低只能看到创建人或执行人是自己的待办事项 filteredTodos = filteredTodos.filter(todo => { return todo.adduser === this.currentUser.username || todo.exeuser === this.currentUser.username; }); } } // 然后根据项目/子项目筛选 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); } return filteredTodos; } 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; } .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 userLevel = this.currentUser.level || 0; let filteredTodos = this.todos; // 等级4可以看到所有待办事项 if (userLevel !== 4) { // 等级3不能看到创建人和执行人都是等级4的待办事项 if (userLevel === 3) { filteredTodos = 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); }); } else { // 等级2或更低只能看到创建人或执行人是自己的待办事项 filteredTodos = filteredTodos.filter(todo => { return todo.adduser === this.currentUser.username || todo.exeuser === this.currentUser.username; }); } } // 构建并渲染项目树(使用过滤后的待办事项) const tree = TodoTree.buildProjectTree(filteredTodos); 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() }); // 然后根据项目/子项目筛选 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 todosContainer = TodoList.renderTodoList(filteredTodos, { onEditExecutor: (todo) => this.showEditExecutorModal(todo), onEditSchedule: (todo) => this.showEditScheduleModal(todo), onEditTitle: (todo) => { // 权限检查 const currentUserLevel = this.currentUser.level || 0; if (currentUserLevel <= 2 && todo.adduser !== this.currentUser.username) { this.showError('您没有权限编辑他人创建的待办事项'); return; } if (currentUserLevel === 3 && todo.adduser === 'admin') { this.showError('您没有权限编辑管理员创建的待办事项'); return; } const { modal, style } = TodoModal.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 { await TodoService.updateTodo(todo.id, { ...todo, title: newTitle }); await this.fetchTodos(); this.render(); modal.remove(); style.remove(); } catch (error) { console.error('更新标题时发生错误:', error); this.showError('更新标题失败,请稍后重试'); } }); document.body.appendChild(modal); }, onEditDescription: (todo) => { // 权限检查 const currentUserLevel = this.currentUser.level || 0; if (currentUserLevel <= 2 && todo.adduser !== this.currentUser.username) { this.showError('您没有权限编辑他人创建的待办事项'); return; } if (currentUserLevel === 3 && todo.adduser === 'admin') { this.showError('您没有权限编辑管理员创建的待办事项'); return; } const { modal, style } = TodoModal.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 { await TodoService.updateTodo(todo.id, { ...todo, text: newText }); await this.fetchTodos(); this.render(); modal.remove(); style.remove(); } catch (error) { console.error('更新描述时发生错误:', error); this.showError('更新描述失败,请稍后重试'); } }); document.body.appendChild(modal); }, onDelete: async (todo) => { // 权限检查 const currentUserLevel = this.currentUser.level || 0; if (currentUserLevel <= 2 && todo.adduser !== this.currentUser.username) { this.showError('您没有权限删除他人创建的待办事项'); return; } if (currentUserLevel === 3 && todo.adduser === 'admin') { this.showError('您没有权限删除管理员创建的待办事项'); return; } if (confirm('确定要删除这条待办事项吗?')) { try { await TodoService.deleteTodo(todo.id); await this.fetchTodos(); this.render(); } catch (error) { console.error('删除待办事项时发生错误:', error); this.showError('删除待办事项失败,请稍后重试'); } } }, onComplete: async (todo) => { const action = todo.completed ? '取消完成' : '标记完成'; if (confirm(`确定要${action}这条待办事项吗?`)) { try { await TodoService.updateTodo(todo.id, { ...todo, completed: !todo.completed, complete_time: !todo.completed ? new Date().toISOString() : null }); await this.fetchTodos(); this.render(); } catch (error) { console.error(`${action}待办事项时发生错误:`, error); this.showError(`${action}待办事项失败,请稍后重试`); } } }, onToggleDescription: (descriptionContent, toggleButton) => { const isExpanded = descriptionContent.classList.contains('expanded'); if (isExpanded) { descriptionContent.classList.remove('expanded'); toggleButton.classList.remove('expanded'); } else { descriptionContent.classList.add('expanded'); toggleButton.classList.add('expanded'); } } }); container.appendChild(treeContainer); container.appendChild(todosContainer); this.shadowRoot.appendChild(container); } connectedCallback() { this.initialize(); } // 清除所有数据 clearData() { 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; } } customElements.define('todo-component', TodoComponent);