feat: 删除部门模块 + 任务系统重构 + 布局修复

- 删除部门(Home)侧边栏入口、面板及相关代码
- 删除 home.js,chartOptions 等图表工具迁移到 utils.js
- 清理 render()、togglePhase、filterPhaseTasks 等死代码
- 任务列表改为平铺(取消阶段分组),支持拖拽排序
- 任务表单加开始时间字段,重组字段布局为三列
- 任务展开面板改为竖排:时间、任务进展、卡点与备注
- 视图按钮和新增任务按钮移入任务卡片顶部
- 修复 index.html div 嵌套不平衡导致布局塌陷
- 修复 savedTab 校验和 switchTenant 侧边栏 active 同步
- 总览工作台只显示预算/发生总览入口
- 性能面板 id 去重
This commit is contained in:
mac
2026-07-15 14:49:12 +08:00
parent 39513c0a7b
commit ec457e9d40
9 changed files with 227 additions and 371 deletions

View File

@@ -173,14 +173,6 @@ window.selectProject = (id) => {
renderProjectTasks(id);
};
window.togglePhase = (phaseId) => {
const wrap = document.querySelector(`#${phaseId}`);
if (!wrap) return;
wrap.classList.toggle("collapsed");
const toggle = document.querySelector(`#${phaseId}-toggle`);
if (toggle) toggle.style.transform = wrap.classList.contains("collapsed") ? "rotate(-90deg)" : "";
};
window.showProjectContext = (event, id) => {
event.preventDefault();
event.stopPropagation();
@@ -306,7 +298,7 @@ function renderProjects() {
pending: tasks.filter(t => t.status === '未开始').length,
};
document.querySelector("#projects").innerHTML = /*html*/`
<div class="grid grid-cols-5 gap-3 mb-4">
<div id="projectHeadCards" class="grid grid-cols-5 gap-3 mb-4">
${[
["项目总数", items.length, "folder"],
["任务总数", taskStats.total, "list-checks"],
@@ -317,15 +309,6 @@ function renderProjects() {
<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="${icon}" style="width:14px;height:14px"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></div>
`).join("")}
</div>
<div class="flex justify-between items-center mb-3">
<div class="flex items-center gap-1" id="taskViewToggle">
<button class="btn btn-sm ${state.taskView === 'compact' ? 'btn-primary' : 'btn-ghost'} p-1.5" onclick="setTaskView('compact')" title="标题视图"><i data-lucide="list" style="width:16px;height:16px"></i></button>
<button class="btn btn-sm ${state.taskView !== 'compact' ? 'btn-primary' : 'btn-ghost'} p-1.5" onclick="setTaskView('detail')" title="详细视图"><i data-lucide="align-left" style="width:16px;height:16px"></i></button>
</div>
<button class="btn btn-primary btn-sm" onclick="openTaskFormForSelected()">
<i data-lucide="plus"></i>新增任务
</button>
</div>
<div class="project-board">
<div class="project-board-body">
<div class="project-tree">
@@ -349,6 +332,15 @@ function renderProjects() {
</div>
</div>
<div class="task-feed">
<div class="flex justify-between items-center px-4 py-2 border-b border-slate-100 bg-white" style="flex-shrink:0">
<div class="flex items-center gap-1" id="taskViewToggle">
<button class="btn btn-sm ${state.taskView === 'compact' ? 'btn-primary' : 'btn-ghost'} p-1.5" onclick="setTaskView('compact')" title="标题视图"><i data-lucide="list" style="width:16px;height:16px"></i></button>
<button class="btn btn-sm ${state.taskView !== 'compact' ? 'btn-primary' : 'btn-ghost'} p-1.5" onclick="setTaskView('detail')" title="详细视图"><i data-lucide="align-left" style="width:16px;height:16px"></i></button>
</div>
<button class="btn btn-primary btn-sm" onclick="openTaskFormForSelected()">
<i data-lucide="plus"></i>新增任务
</button>
</div>
${state.selectedProject ? '<div class="task-feed-body">' + renderTaskListHTML(state.selectedProject) + '</div>' : `
<div class="project-empty">
<div class="text-center">
@@ -381,44 +373,33 @@ function renderTaskListHTML(projectId) {
const project = state.data.operations.find((x) => x.id === projectId);
if (!project) return "";
const tasks = (state.data.tasks || []).filter((t) => t.project_id === projectId);
const filtered = tasks;
const defaultPhases = ["商务洽谈", "系统上线", "团队分工", "项目交付", "上线推广", "结项验收"];
const customPhases = [...new Set(filtered.map(t => t.phase).filter(Boolean))];
const phaseOrder = [...defaultPhases];
customPhases.forEach(p => { if (!phaseOrder.includes(p)) phaseOrder.push(p); });
const phases = phaseOrder.filter(p => filterPhaseTasks(filtered, p).length > 0);
const phaseTasks = phases.map(p => ({ phase: p, tasks: filterPhaseTasks(filtered, p) }));
const taskItem = (t) => `<div class="task-item ${t.status === '已结束' ? 'task-done' : ''} ${t.priority === 'P0' ? 'task-p0' : t.priority === 'P1' ? 'task-p1' : ''} ${state.taskView === 'detail' ? 'task-detail' : ''}" data-id="${t.id}" draggable="true" ondragstart="handleTaskDragStart(event, ${t.id})" ondragend="event.currentTarget.classList.remove('dragging')" onclick="openTaskForm(${projectId}, ${t.id})">
<span class="task-grip"><i data-lucide="grip-vertical"></i></span>
<span class="task-status-badge status-${esc(t.status) || '未开始'}" onclick="event.stopPropagation(); cycleTaskStatus(${t.id}, ${projectId})" title="点击切换状态">${esc(t.status) || '未开始'}</span>
<span class="task-priority-badge priority-${(t.priority || 'P2').toLowerCase()}" onclick="event.stopPropagation(); cycleTaskPriority(${t.id}, ${projectId})" title="点击切换优先级">${esc(t.priority) || 'P2'}</span>
<div class="task-content">
<span class="task-title">${esc(t.task)}</span>
${state.taskView === 'detail' && t.notes ? '<span class="task-desc">' + esc(t.notes) + '</span>' : ""}
${state.taskView === 'detail' && t.blockers ? '<span class="task-blocker">\u26a0 ' + esc(t.blockers) + '</span>' : ""}
</div>
<span class="task-meta">${esc(t.owner) || ''}</span>
<span class="task-expand-toggle" onclick="event.stopPropagation();toggleTaskExpand(${t.id})"><i data-lucide="chevron-down" id="tke_icon_${t.id}"></i></span>
</div>
<div class="task-expand-body hidden" id="tke_body_${t.id}">
<div class="flex flex-col gap-3 px-6 py-3 text-sm">
<div><span class="text-slate-400 text-xs">时间</span><p class="text-slate-700 mt-1">${esc(t.start_date) || '—'}${esc(t.due_date) || '—'}</p></div>
<div><span class="text-slate-400 text-xs">任务进展</span><p class="text-slate-700 mt-1">${esc(t.notes) || '—'}</p></div>
<div><span class="text-slate-400 text-xs">卡点与备注</span><p class="${t.blockers ? 'text-red-600' : 'text-slate-700'} mt-1">${t.blockers ? '\u26a0 ' + esc(t.blockers) : '—'}</p></div>
</div>
</div>`;
return `
${phaseTasks.map(({ phase, tasks: pt }) => {
if (!pt.length) return "";
const phaseId = "phase-" + projectId + "-" + phase.replace(/\s/g, "");
return `<div class="task-section">
<div class="task-section-hd" onclick="togglePhase('${phaseId}')">
<span class="task-section-toggle" id="${phaseId}-toggle"><i data-lucide="chevron-down"></i></span>
<span class="task-section-icon"><i data-lucide="layers"></i></span>
<span class="task-section-label">${phase}</span>
<span class="task-section-n">${pt.length}</span>
</div>
<div class="task-section-list-wrap" id="${phaseId}">
<div class="task-section-list" data-phase="${phase}" ondrop="handleTaskDrop(event, ${projectId}, '${phase}')" ondragover="event.preventDefault(); event.currentTarget.classList.add('drag-over')" ondragleave="event.currentTarget.classList.remove('drag-over')">
${pt.map((t) => `<div class="task-item ${t.status === '已结束' ? 'task-done' : ''} ${t.priority === 'P0' ? 'task-p0' : t.priority === 'P1' ? 'task-p1' : ''} ${state.taskView === 'detail' ? 'task-detail' : ''}" data-id="${t.id}" draggable="true" ondragstart="handleTaskDragStart(event, ${t.id})" ondragend="event.currentTarget.classList.remove('dragging')">
<span class="task-grip"><i data-lucide="grip-vertical"></i></span>
<span class="task-status-badge status-${esc(t.status) || '未开始'}" onclick="event.stopPropagation(); cycleTaskStatus(${t.id}, ${projectId})" title="点击切换状态">${esc(t.status) || '未开始'}</span>
<span class="task-priority-badge priority-${(t.priority || 'P2').toLowerCase()}" onclick="event.stopPropagation(); cycleTaskPriority(${t.id}, ${projectId})" title="点击切换优先级">${esc(t.priority) || 'P2'}</span>
<div class="task-content" onclick="openTaskForm(${projectId}, ${t.id})">
<span class="task-title">${esc(t.task)}</span>
${state.taskView === 'detail' && t.notes ? '<span class="task-desc">' + esc(t.notes) + '</span>' : ""}
${state.taskView === 'detail' && t.blockers ? '<span class="task-blocker">\u26a0 ' + esc(t.blockers) + '</span>' : ""}
</div>
<span class="task-meta">${esc(t.owner) || ''}</span>
<span class="task-meta text-slate-400">${esc(t.due_date) || ''}</span>
</div>`).join("")}
</div>
</div>
</div>`;
}).join("")}
${filtered.length === 0 ? '<div class="task-empty">暂无任务,点击上方按钮创建</div>' : ''}
<div class="task-section-list-wrap">
<div class="task-section-list" ondrop="handleTaskDrop(event, ${projectId}, '')" ondragover="event.preventDefault(); event.currentTarget.classList.add('drag-over')" ondragleave="event.currentTarget.classList.remove('drag-over')">
${tasks.length ? tasks.map(taskItem).join("") : '<div class="task-empty">暂无任务,点击上方按钮创建</div>'}
</div>
</div>
`;
}
@@ -440,48 +421,56 @@ window.openTaskFormForSelected = () => {
window.openTaskForm = (projectId, taskId) => {
if (!projectId) return;
// 确保 drawer 存在
let drawer = document.querySelector(`#task-drawer-${projectId}`);
if (!drawer) {
drawer = document.createElement("div");
drawer.id = `task-drawer-${projectId}`;
drawer.className = "task-drawer";
document.body.appendChild(drawer);
}
const tasks = (state.data.tasks || []).filter((t) => t.project_id === projectId);
const defaultPhases = ["商务洽谈", "系统上线", "团队分工", "项目交付", "上线推广", "结项验收"];
const customPhases = [...new Set(tasks.map(t => t.phase).filter(Boolean))];
const phases = [...new Set([...defaultPhases, ...customPhases])];
const modal = document.getElementById("taskModal");
if (!modal) return;
const task = taskId ? (state.data.tasks || []).find((t) => t.id === taskId) : null;
drawer.innerHTML = `<div class="task-drawer-hd"><span class="task-drawer-title">${task ? "编辑任务" : "新增任务"}</span><div class="flex items-center gap-2">${task ? `<button type="button" class="btn btn-ghost btn-sm text-red-600 hover:bg-red-50" onclick="deleteTask(${projectId})"><i data-lucide="trash-2"></i>删除</button>` : ""}<button class="task-close" onclick="closeTaskDrawer(${projectId})"><i data-lucide="x"></i></button></div></div>
<form class="task-drawer-form" onsubmit="submitTaskForm(event, ${projectId})">
<input type="hidden" name="task_id" id="task-id-${projectId}" value="${task ? task.id : ''}">
<div class="task-field-row">
<label class="task-field"><span>任务名称</span><input name="task" required id="task-name-${projectId}" class="form-ctrl" value="${task ? esc(task.task) : ''}"></label>
<label class="task-field"><span>任务分组</span><select name="phase" id="task-phase-${projectId}" class="form-ctrl">${phases.map((p) => `<option ${task && task.phase === p ? "selected" : ""}>${p}</option>`).join("")}</select></label>
modal.className = "hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40";
modal.innerHTML = `<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto" onclick="event.stopPropagation()">
<div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-6 py-4 flex items-center justify-between">
<h3 class="text-lg font-bold text-slate-800">${task ? "编辑任务" : "新增任务"}</h3>
<div class="flex items-center gap-2">
${task ? '<button type="button" class="btn btn-ghost btn-sm text-red-600 hover:bg-red-50" id="taskDelBtn"><i data-lucide="trash-2"></i>删除</button>' : ""}
<button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" id="taskClsBtn"><i data-lucide="x"></i></button>
</div>
<div class="task-field-row">
<label class="task-field"><span>优先级</span><select name="priority" id="task-priority-${projectId}" class="form-ctrl"><option ${task && task.priority === 'P0' ? 'selected' : ''}>P0</option><option ${task && task.priority === 'P1' ? 'selected' : ''}>P1</option><option ${(!task || task.priority === 'P2') ? 'selected' : ''}>P2</option><option ${task && task.priority === 'P3' ? 'selected' : ''}>P3</option></select></label>
<label class="task-field"><span>状态</span><select name="status" id="task-status-${projectId}" class="form-ctrl"><option ${(!task || task.status === '') ? 'selected' : ''}></option><option ${task && task.status === '' ? 'selected' : ''}></option><option ${task && task.status === '' ? 'selected' : ''}></option></select></label>
</div>
<form class="px-6 py-4 flex flex-col gap-4" onsubmit="event.preventDefault()">
<input type="hidden" name="task_id" value="${task ? task.id : ''}">
<div class="grid grid-cols-2 gap-3">
<label class="task-field"><span>任务名称</span><input name="task" required class="form-ctrl" value="${task ? esc(task.task) : ''}"></label>
<label class="task-field"><span>优先级</span><select name="priority" class="form-ctrl"><option ${task && task.priority === 'P0' ? 'selected' : ''}>P0</option><option ${task && task.priority === 'P1' ? 'selected' : ''}>P1</option><option ${(!task || task.priority === 'P2') ? 'selected' : ''}>P2</option><option ${task && task.priority === 'P3' ? 'selected' : ''}>P3</option></select></label>
</div>
<div class="task-field-row">
<label class="task-field"><span>负责人</span><input name="owner" id="task-owner-${projectId}" class="form-ctrl" value="${task ? esc(task.owner) : ''}"></label>
<label class="task-field"><span>截止时间</span><input name="due_date" type="date" id="task-due-${projectId}" class="form-ctrl" value="${task ? esc(task.due_date) : ''}"></label>
<div class="grid grid-cols-2 gap-3">
<label class="task-field"><span>状态</span><select name="status" class="form-ctrl"><option ${(!task || task.status === '未开始') ? 'selected' : ''}>未开始</option><option ${task && task.status === '进行中' ? 'selected' : ''}>进行中</option><option ${task && task.status === '已结束' ? 'selected' : ''}>已结束</option></select></label>
</div>
<label class="task-field"><span>任务说明</span><textarea name="notes" rows="3" id="task-notes-${projectId}" class="form-ctrl">${task ? esc(task.notes) : ''}</textarea></label>
<label class="task-field"><span>卡点&备注</span><textarea name="blockers" rows="2" id="task-blockers-${projectId}" class="form-ctrl" placeholder="">${task ? esc(task.blockers) : ''}</textarea></label>
<div class="flex justify-end gap-2 mt-4 pt-3 border-t border-slate-100">
<button type="button" class="btn btn-ghost btn-sm" onclick="closeTaskDrawer(${projectId})">取消</button>
<button type="submit" class="btn btn-primary btn-sm" id="task-submit-btn-${projectId}">${task ? "保存" : "确认新增"}</button>
<div class="grid grid-cols-3 gap-3">
<label class="task-field"><span>负责人</span><input name="owner" class="form-ctrl" value="${task ? esc(task.owner) : ''}"></label>
<label class="task-field"><span>开始时间</span><input name="start_date" type="date" class="form-ctrl" value="${task ? esc(task.start_date) : ''}"></label>
<label class="task-field"><span>截止时间</span><input name="due_date" type="date" class="form-ctrl" value="${task ? esc(task.due_date) : ''}"></label>
</div>
</form>`;
drawer.classList.add("open");
<label class="task-field"><span>任务进展</span><textarea name="notes" rows="3" class="form-ctrl">${task ? esc(task.notes) : ''}</textarea></label>
<label class="task-field"><span>卡点&备注</span><textarea name="blockers" rows="2" class="form-ctrl" placeholder="风险卡点、依赖项等">${task ? esc(task.blockers) : ''}</textarea></label>
<div class="flex justify-end gap-2 pt-3 border-t border-slate-100">
<button type="button" class="btn btn-ghost btn-sm" id="taskCancelBtn">取消</button>
<button type="submit" class="btn btn-primary btn-sm" id="taskSubmitBtn">${task ? "保存" : "确认新增"}</button>
</div>
</form>
</div>`;
// 绑定事件
var delBtn = document.getElementById("taskDelBtn");
if (delBtn) delBtn.onclick = function() { deleteTask(projectId); };
var clsBtn = document.getElementById("taskClsBtn");
if (clsBtn) clsBtn.onclick = function() { closeTaskDrawer(projectId); };
var cancelBtn = document.getElementById("taskCancelBtn");
if (cancelBtn) cancelBtn.onclick = function() { closeTaskDrawer(projectId); };
var submitBtn = document.getElementById("taskSubmitBtn");
if (submitBtn) submitBtn.onclick = function(e) { e.preventDefault(); submitTaskForm(e, projectId); };
modal.classList.remove("hidden");
if (window.lucide) window.lucide.createIcons();
};
window.closeTaskDrawer = (projectId) => {
const drawer = document.querySelector(`#task-drawer-${projectId}`);
if (drawer) drawer.classList.remove("open");
const modal = document.getElementById("taskModal");
if (modal) modal.classList.add("hidden");
refreshTaskList(projectId);
};
@@ -492,10 +481,34 @@ window.refreshTaskList = (projectId) => {
if (window.lucide) window.lucide.createIcons();
}
};
window.refreshProjectHeadCards = () => {
const container = document.querySelector("#projectHeadCards");
if (!container) return;
const items = state.data.operations || [];
const tasks = state.data.tasks || [];
const stats = {
total: tasks.length,
ongoing: tasks.filter(t => t.status === '进行中').length,
done: tasks.filter(t => t.status === '已结束').length,
pending: tasks.filter(t => t.status === '未开始').length,
};
const cards = [
["项目总数", items.length, "folder"],
["任务总数", stats.total, "list-checks"],
["进行中", stats.ongoing, "play-circle"],
["已结束", stats.done, "check-circle"],
["未开始", stats.pending, "circle"],
];
container.innerHTML = cards.map(([label, value, icon]) => `
<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="${icon}" style="width:14px;height:14px"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></div>
`).join("");
if (window.lucide) window.lucide.createIcons();
};
window.submitTaskForm = async (event, projectId) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
const form = document.getElementById("taskModal").querySelector("form");
const data = Object.fromEntries(new FormData(form).entries());
data.project_id = Number(projectId);
data.tenant = state.tenant;
const taskId = data.task_id;
@@ -507,11 +520,14 @@ window.submitTaskForm = async (event, projectId) => {
if (task) Object.assign(task, data);
if (data.task) logActivity("task", taskId, "更新了任务「" + data.task + "」");
closeTaskDrawer(projectId);
refreshProjectHeadCards();
} else {
const result = await api("/api/tasks", { method: "POST", body: JSON.stringify({ data }) });
if (result.id && data.task) logActivity("task", result.id, "创建了任务「" + data.task + "」");
if (!state.data.tasks) state.data.tasks = [];
state.data.tasks.push({ id: result.id, ...data });
closeTaskDrawer(projectId);
try { state.data.tasks = await api("/api/tasks/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {}
refreshProjectHeadCards();
}
} catch (error) {
toast("保存失败:" + error.message, "error");
@@ -537,6 +553,7 @@ window.cycleTaskStatus = async (taskId, projectId) => {
badge.className = "task-status-badge status-" + newStatus;
}
}
refreshProjectHeadCards();
} catch (error) {
toast("更新失败:" + error.message, "error");
}
@@ -568,8 +585,25 @@ window.cycleTaskPriority = async (taskId, projectId) => {
}
};
window.toggleTaskExpand = (taskId) => {
const body = document.getElementById("tke_body_" + taskId);
const icon = document.getElementById("tke_icon_" + taskId);
if (!body || !icon) return;
const isHidden = body.classList.contains("hidden");
if (isHidden) {
body.classList.remove("hidden");
icon.style.transform = "rotate(180deg)";
} else {
body.classList.add("hidden");
icon.style.transform = "";
}
if (window.lucide) window.lucide.createIcons();
};
window.deleteTask = async (projectId) => {
const taskId = document.querySelector(`#task-id-${projectId}`).value;
const modal = document.getElementById("taskModal");
if (!modal) return;
const taskId = modal.querySelector('input[name="task_id"]').value;
if (!taskId) return;
if (!confirm("确认删除该任务?此操作不可撤销。")) return;
try {
@@ -581,6 +615,7 @@ window.deleteTask = async (projectId) => {
state.data.tasks = (state.data.tasks || []).filter(t => t.id !== parseInt(taskId));
const row = document.querySelector(`.task-item[data-id="${taskId}"]`);
if (row) row.remove();
refreshProjectHeadCards();
} catch (error) {
toast("删除失败:" + error.message, "error");
}