Compare commits

...

6 Commits

2 changed files with 57 additions and 8 deletions

View File

@@ -170,6 +170,22 @@ window.createSales = (event) => createResource(event, "sales");
window.createProposal = (event) => createResource(event, "proposals"); window.createProposal = (event) => createResource(event, "proposals");
window.createOperation = (event) => createResource(event, "operations"); window.createOperation = (event) => createResource(event, "operations");
window.createProduct = (event) => createResource(event, "products"); window.createProduct = (event) => createResource(event, "products");
async function createTask(event, projectId) {
event.preventDefault();
const form = event.currentTarget;
const data = Object.fromEntries(new FormData(form).entries());
data.project_id = projectId;
try {
await api("/api/tasks", { method: "POST", body: JSON.stringify({ data }) });
form.reset();
form.classList.add("hidden");
await load();
showTaskModal(projectId);
} catch (error) {
alert("创建失败:" + error.message);
}
}
window.createFinance = (event) => createResource(event, "finance"); window.createFinance = (event) => createResource(event, "finance");
window.switchTab = switchTab; window.switchTab = switchTab;
@@ -208,14 +224,31 @@ function showTaskModal(projectId) {
const project = state.data.operations.find((x) => x.id === projectId); const project = state.data.operations.find((x) => x.id === projectId);
const tasks = (state.data.tasks || []).filter((t) => t.project_id === projectId); const tasks = (state.data.tasks || []).filter((t) => t.project_id === projectId);
const phases = ["项目准备", "项目执行", "项目验收", "验收完毕"]; const phases = ["项目准备", "项目执行", "项目验收", "验收完毕"];
document.querySelector("#taskModal").innerHTML = `<div class="task-overlay" onclick="closeTaskModal()"><div class="task-panel" onclick="event.stopPropagation()"><div class="task-header"><h2 class="task-title">${project.project_name} · 任务清单</h2><button class="task-close" onclick="closeTaskModal()"><i data-lucide="x"></i></button></div><div class="task-body">${phases.map((phase) => { document.querySelector("#taskModal").innerHTML = `<div class="task-overlay" onclick="closeTaskModal()"><div class="task-panel" onclick="event.stopPropagation()"><div class="task-header"><h2 class="task-title">${project.project_name} · 任务清单</h2><div class="flex items-center gap-3"><button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); document.querySelector('#task-form-${projectId}').classList.toggle('hidden')"><i data-lucide="plus"></i>新增任务</button><button class="task-close" onclick="closeTaskModal()"><i data-lucide="x"></i></button></div></div><div class="task-body">
<form id="task-form-${projectId}" class="hidden task-form" onsubmit="createTask(event, ${projectId})">
<div class="grid grid-cols-2 gap-3">
<label class="task-field"><span>任务名称</span><input name="task" required></label>
<label class="task-field"><span>任务阶段</span><select name="phase">${phases.map((p) => `<option>${p}</option>`).join("")}</select></label>
<label class="task-field"><span>负责人</span><input name="owner"></label>
<label class="task-field"><span>截止时间</span><input name="due_date" type="date"></label>
<label class="task-field col-span-2"><span>任务说明</span><textarea name="notes" rows="2"></textarea></label>
</div>
<div class="flex justify-end gap-2 mt-3">
<button type="button" class="btn btn-ghost btn-sm" onclick="document.querySelector('#task-form-${projectId}').classList.add('hidden')">取消</button>
<button type="submit" class="btn btn-primary btn-sm">确认新增</button>
</div>
</form>
${phases.map((phase) => {
const pt = tasks.filter((t) => t.phase === phase); const pt = tasks.filter((t) => t.phase === phase);
return `<div class="task-group"><div class="task-group-hd"><span class="task-group-icon"><i data-lucide="layers"></i></span><span class="task-group-label">${phase}</span><span class="task-group-n">${pt.length}</span></div><div class="task-group-list">${pt.length ? pt.map((t) => `<div class="task-row" data-id="${t.id}"><span class="task-dot"><i data-lucide="${t.status === 'done' ? 'check-circle' : 'circle'}"></i></span><span class="task-name">${t.task}</span></div>`).join("") : `<div class="task-none">暂无任务</div>`}</div><button class="task-group-add" onclick="event.stopPropagation()">+ 添加任务</button></div>`; return `<div class="task-group"><div class="task-group-hd"><span class="task-group-icon"><i data-lucide="layers"></i></span><span class="task-group-label">${phase}</span><span class="task-group-n">${pt.length}</span></div><div class="task-group-list">${pt.length ? pt.map((t) => `<div class="task-row" data-id="${t.id}"><span class="task-dot"><i data-lucide="${t.status === 'done' ? 'check-circle' : 'circle'}"></i></span><div class="task-main"><span class="task-name">${t.task}</span>${t.notes ? `<span class="task-desc">${t.notes}</span>` : ""}</div><div class="task-right"><span class="task-owner">👤 ${t.owner || ""}</span><span class="task-date">📅 ${t.due_date || ""}</span></div></div>`).join("") : `<div class="task-none">暂无任务</div>`}</div></div>`;
}).join("")}</div></div></div>`; }).join("")}</div></div></div>`;
document.querySelector("#taskModal").classList.add("active"); document.querySelector("#taskModal").classList.add("active");
if (window.lucide) window.lucide.createIcons(); if (window.lucide) window.lucide.createIcons();
} }
window.closeTaskModal = () => document.querySelector("#taskModal").classList.remove("active"); window.closeTaskModal = () => {
document.querySelector("#taskModal").classList.remove("active");
document.querySelector("#taskModal").innerHTML = "";
};
function renderProposals() { function renderProposals() {
const proposalRows = state.data.proposals.map((p) => [p.customer_or_project_name, p.version, badge(p.status), p.files.length + " 个"]); const proposalRows = state.data.proposals.map((p) => [p.customer_or_project_name, p.version, badge(p.status), p.files.length + " 个"]);

View File

@@ -499,7 +499,7 @@ td {
padding-top: 48px; overflow-y: auto; padding-top: 48px; overflow-y: auto;
} }
.task-panel { .task-panel {
background: #18191c; border-radius: 12px; width: 600px; max-width: 92vw; background: #18191c; border-radius: 12px; width: 800px; max-width: 94vw;
box-shadow: 0 24px 80px rgba(0,0,0,0.4); margin-bottom: 48px; box-shadow: 0 24px 80px rgba(0,0,0,0.4); margin-bottom: 48px;
} }
.task-header { .task-header {
@@ -529,12 +529,28 @@ td {
} }
.task-group-list { display: flex; flex-direction: column; } .task-group-list { display: flex; flex-direction: column; }
.task-row { .task-row {
display: flex; align-items: center; gap: 10px; display: flex; align-items: flex-start; gap: 10px;
padding: 8px 14px; border-top: 1px solid #24272d; padding: 10px 14px; border-top: 1px solid #24272d;
} }
.task-dot { display: flex; color: #4b4d54; } .task-dot { display: flex; color: #4b4d54; padding-top: 2px; flex-shrink: 0; }
.task-name { color: #c5c6ca; font-size: 13px; } .task-main { flex: 1; min-width: 0; }
.task-name { color: #c5c6ca; font-size: 13px; display: block; }
.task-desc { color: #5a5c63; font-size: 12px; display: block; margin-top: 3px; }
.task-right { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0; }
.task-owner, .task-date { color: #898b94; font-size: 12px; white-space: nowrap; }
.task-none { color: #4b4d54; font-size: 13px; padding: 12px 14px; text-align: center; border-top: 1px solid #24272d; } .task-none { color: #4b4d54; font-size: 13px; padding: 12px 14px; text-align: center; border-top: 1px solid #24272d; }
.task-form {
background: #141518; border: 1px solid #2a2d34; border-radius: 8px;
padding: 14px; margin-bottom: 16px;
}
.task-field { display: flex; flex-direction: column; gap: 4px; }
.task-field span { color: #6b6d75; font-size: 12px; }
.task-field input, .task-field select, .task-field textarea {
background: #1e2025; border: 1px solid #2a2d34; border-radius: 6px;
color: #e4e5e7; font-size: 13px; padding: 6px 10px; outline: none;
}
.task-field input:focus, .task-field select:focus, .task-field textarea:focus { border-color: #4a6cf7; }
.col-span-2 { grid-column: span 2; }
.task-group-add { .task-group-add {
display: block; width: 100%; padding: 10px; text-align: center; display: block; width: 100%; padding: 10px; text-align: center;
color: #6b6d75; font-size: 13px; background: none; border: none; color: #6b6d75; font-size: 13px; background: none; border: none;