XNSim/XNSimHtml/components/todo-component.js

1895 lines
69 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; // 添加初始化标志
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 = `
<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);
treeContent.appendChild(projectNode);
});
// 创建底部控制区域
const bottomControls = document.createElement('div');
bottomControls.className = 'bottom-controls';
// 添加显示已完成复选框
const showCompletedContainer = document.createElement('div');
showCompletedContainer.className = 'show-completed-container';
showCompletedContainer.innerHTML = `
<input type="checkbox" id="show-completed" ${this.showCompleted ? 'checked' : ''}>
<label for="show-completed">显示已完成</label>
`;
// 添加复选框事件监听
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;
}
.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 = `
<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>
`;
} else if (type === 'schedule') {
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="schedule">选择计划时间:</label>
<input type="datetime-local" id="schedule" class="form-control">
</div>
</div>
<div class="modal-footer">
<button class="save-button">保存</button>
<button class="cancel-button">取消</button>
</div>
</div>
`;
} else if (type === 'new-todo') {
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="title">标题:</label>
<input type="text" id="title" class="form-control" required placeholder="请输入待办标题">
</div>
<div class="form-group">
<label for="project">项目:</label>
<div class="custom-select">
<input type="text" id="project" class="form-control" placeholder="请选择或输入项目">
<div class="select-options" id="project-options"></div>
</div>
</div>
<div class="form-group">
<label for="subproject">子项目:</label>
<div class="custom-select">
<input type="text" id="subproject" class="form-control" placeholder="请选择或输入子项目">
<div class="select-options" id="subproject-options"></div>
</div>
</div>
<div class="form-group">
<label for="text">详细描述:</label>
<textarea id="text" class="form-control"></textarea>
</div>
<div class="form-group">
<label for="schedule">计划时间:</label>
<input type="datetime-local" id="schedule" class="form-control" required>
</div>
<div class="form-group">
<label for="executor">执行人:</label>
<select id="executor" class="form-control" required>
<option value="">请选择执行人</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="save-button">保存</button>
<button class="cancel-button">取消</button>
</div>
</div>
`;
} else if (type === 'edit-title') {
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="title">标题:</label>
<input type="text" id="title" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button class="save-button">保存</button>
<button class="cancel-button">取消</button>
</div>
</div>
`;
} else if (type === 'edit-description') {
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="text">详细描述:</label>
<textarea id="text" class="form-control" rows="6"></textarea>
</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();
});
// 将样式添加到文档头部
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 = `
<div class="todo-header">
<div class="todo-actions">
<button class="complete-button" title="${todo.completed ? '取消完成' : '标记完成'}">
<img src="assets/icons/png/${todo.completed ? 'cancel_b.svg' : 'complete_b.svg'}" alt="${todo.completed ? '取消完成' : '标记完成'}" class="action-icon">
</button>
</div>
<h3>${todo.project} - ${todo.subproject} - ${todo.title}</h3>
<div class="todo-actions">
<button class="edit-button" title="编辑标题">
<img src="assets/icons/png/sliders_b.png" alt="编辑" class="action-icon">
</button>
<button class="delete-button delete-red" title="删除待办">
<img src="assets/icons/png/delete_b.png" alt="删除" class="action-icon">
</button>
<span class="status ${todo.completed ? 'completed' : 'pending'}">
${todo.completed ? '已完成' : '进行中'}
</span>
</div>
</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>
<a href="#" class="schedule-link">${scheDate}</a>
<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">
<div class="description-header">
<span>详细描述:</span>
<div class="description-actions">
<button class="edit-button" title="编辑描述">
<img src="assets/icons/png/sliders_b.png" alt="编辑" class="action-icon">
</button>
<button class="toggle-button" title="展开/收起">
<img src="assets/icons/png/chevron-down_b.png" alt="展开/收起" class="action-icon toggle-icon">
</button>
</div>
</div>
<div class="description-content">${todo.text || '无详细描述'}</div>
</div>
</div>
`;
// 添加执行人链接点击事件
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);