XNSim/XNSimHtml/components/todo-component.js

695 lines
23 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.

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; // 添加初始化标志
}
// 初始化组件
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';
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 = `
<div class="project-header-content">
<span class="expand-icon ${this.expandedProjects.has(project.name) ? 'expanded' : ''}">▶</span>
<span class="project-name">${project.name}</span>
<span class="todo-count">(${Object.values(project.subprojects).reduce((sum, sub) =>
sum + sub.todos.length, 0)})</span>
</div>
`;
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 = `
<span class="subproject-name">${subproject.name}</span>
<span class="todo-count">(${subproject.todos.length})</span>
`;
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);
treeContainer.appendChild(projectNode);
});
// 添加显示全部按钮
const showAllButton = document.createElement('button');
showAllButton.className = 'show-all-button';
showAllButton.textContent = '显示全部';
showAllButton.addEventListener('click', () => {
this.selectedProject = null;
this.selectedSubproject = null;
this.render();
});
treeContainer.appendChild(showAllButton);
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;
});
}
// 根据用户等级进行筛选
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;
});
}
// 创建模态对话框
createModal() {
// 创建样式
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-control {
width: 100%;
padding: 8px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
}
.save-button, .cancel-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.save-button {
background-color: #007bff;
color: white;
}
.save-button:hover {
background-color: #0056b3;
}
.cancel-button {
background-color: #6c757d;
color: white;
}
.cancel-button:hover {
background-color: #5a6268;
}
`;
document.head.appendChild(style);
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>编辑执行人</h3>
<button class="close-button">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="executor">选择执行人:</label>
<select id="executor" class="form-control">
<option value="">请选择执行人</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="save-button">保存</button>
<button class="cancel-button">取消</button>
</div>
</div>
`;
// 添加关闭按钮事件
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(); // 移除样式
});
// 添加保存按钮事件
const saveButton = modal.querySelector('.save-button');
saveButton.addEventListener('click', async () => {
const executor = modal.querySelector('#executor').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('更新执行人失败,请稍后重试');
}
}
});
return modal;
}
// 显示编辑执行人对话框
showEditExecutorModal(todo) {
this.editingTodo = todo;
const modal = this.createModal();
// 获取当前用户等级
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);
});
document.body.appendChild(modal);
}
render() {
if (!this.isInitialized) {
return;
}
// 清空现有内容
this.shadowRoot.innerHTML = '';
// 添加样式
const style = document.createElement('style');
style.textContent = `
.container {
display: flex;
height: 100%;
font-family: Arial, sans-serif;
}
.project-tree {
width: 250px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.show-all-button {
margin-top: 15px;
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;
}
.todo-container {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.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;
}
.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;
}
`;
this.shadowRoot.appendChild(style);
// 创建主容器
const container = document.createElement('div');
container.className = 'container';
// 构建并渲染项目树
const tree = this.buildProjectTree();
const treeContainer = this.renderProjectTree(tree);
container.appendChild(treeContainer);
// 创建待办事项列表容器
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 = `
<div class="todo-header">
<h3>${todo.project}-${todo.subproject}</h3>
<span class="status ${todo.completed ? 'completed' : 'pending'}">
${todo.completed ? '已完成' : '进行中'}
</span>
</div>
<div class="todo-content">
<div class="todo-info">
<span class="label">执行人:</span>
<a href="#" class="executor-link">${todo.exeuser || '未分配'}</a>
<span class="spacer"></span>
<span class="label">创建人:</span>
<span class="value">${todo.adduser}</span>
</div>
<div class="todo-info">
<span class="label">计划时间:</span>
<span class="value">${scheDate}</span>
<span class="label">创建时间:</span>
<span class="value">${createdDate}</span>
</div>
${todo.completed ? `
<div class="todo-info">
<span class="label">完成时间:</span>
<span class="value">${completeDate}</span>
</div>
` : ''}
<div class="todo-description">
${todo.text || '无详细描述'}
</div>
</div>
`;
// 添加执行人链接点击事件
const executorLink = todoElement.querySelector('.executor-link');
executorLink.addEventListener('click', (e) => {
e.preventDefault();
this.showEditExecutorModal(todo);
});
todosContainer.appendChild(todoElement);
});
container.appendChild(todosContainer);
this.shadowRoot.appendChild(container);
}
connectedCallback() {
this.initialize();
}
}
customElements.define('todo-component', TodoComponent);