XNSim/XNSimHtml/components/todo-component.js

905 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
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 {
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('获取用户数据失败,请稍后重试');
}
}
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);
}
// 显示编辑执行人对话框
showEditExecutorModal(todo) {
this.editingTodo = todo;
const { modal, style } = TodoModal.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 {
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) {
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 } = TodoModal.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 = 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 [];
}
// 首先根据项目/子项目筛选
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;
});
}
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 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 filteredTodos = this.filterTodos();
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;
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 { 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) => {
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();
}
}
customElements.define('todo-component', TodoComponent);