v1.3.0 — 重点项目简化:6列表格 + 进展按钮弹任务浮层(4阶段)

This commit is contained in:
mac
2026-06-15 12:02:27 +08:00
parent 55e7a3a4dc
commit 938ec8d181
3 changed files with 39 additions and 52 deletions

View File

@@ -174,52 +174,39 @@ window.createFinance = (event) => createResource(event, "finance");
window.switchTab = switchTab;
function renderProjects() {
// Merge sales_leads and operation_projects into one table
const salesItems = state.data.sales.map((x) => ({
name: x.target_customer,
version: "",
type: "opportunity",
status: x.status,
amount: 0,
stage: "",
files: 0,
followup: x.latest_follow_up_record,
resource: "sales",
id: x.id,
}));
const opItems = state.data.operations.map((x) => ({
name: x.project_name,
version: x.project_version,
type: x.project_type,
status: x.project_status,
amount: x.expected_contract_amount || 0,
stage: x.current_stage || x.sop_stage,
files: x.files.length,
followup: x.latest_follow_up_record,
resource: "operations",
id: x.id,
}));
const allItems = [...salesItems, ...opItems];
const items = state.opFilter === "all" ? allItems : allItems.filter((x) => x.type === state.opFilter || (state.opFilter === "opportunity" && x.type === "opportunity"));
const rows = items.map((x) => [`<strong>${x.name}</strong>${x.version ? `<p class="text-xs text-slate-500">${x.version}</p>` : ""}`, badge(x.type), badge(x.status), x.amount ? money(x.amount) : "—", text(x.stage), text(x.followup)]);
const clicks = items.map((x) => ({ resource: x.resource, id: x.id }));
const items = state.data.operations;
const rows = items.map((x) => [
`<strong>${x.project_name}</strong><p class="text-xs text-slate-500">${x.project_version}</p>`,
text(x.customer_need || x.notes),
badge(x.current_stage || x.project_status),
x.expected_contract_amount ? money(x.expected_contract_amount) : "—",
text(x.owner || "—"),
`<button class="btn btn-ghost btn-sm text-blue-600" onclick="event.stopPropagation(); showTaskModal(${x.id})"><i data-lucide="eye"></i>查看</button>`
]);
document.querySelector("#projects").innerHTML = `<div class="grid gap-4">
${card(formHtml([
{ label: "业务机会", input: `<input name="target_customer" required placeholder="客户名称">` },
{ label: "优先级", input: `<select name="priority"><option>P0</option><option selected>P1</option><option>P2</option><option>P3</option></select>` },
{ label: "状态", input: `<select name="status"><option>待跟进</option><option>跟进中</option><option>方案中</option><option>商务谈判</option><option>已签约</option><option>暂缓</option><option>已丢单</option></select>` },
], { handler: "createSales", text: `<i data-lucide="plus"></i>新增业务机会` }), "p-4")}
${card(formHtml([
{ label: "项目名称", input: `<input name="project_name" required>` },
{ label: "项目版本", input: `<input name="project_version" value="v1.0">` },
{ label: "项目类型", input: `<select name="project_type"><option value="opportunity">业务机会项目</option><option value="execution">已签约执行项目</option></select>` },
{ label: "状态", input: `<input name="project_status" value="线索发现">` },
{ label: "当前阶段", input: `<select name="current_stage"><option>项目准备</option><option></option><option></option><option></option></select>` },
{ label: "项目金额", input: `<input name="expected_contract_amount" type="number" step="0.01" placeholder="万元">` },
{ label: "负责人", input: `<input name="owner">` },
], { handler: "createOperation", text: "新增项目" }), "p-4")}
<div class="flex gap-2">${[["all","全部"],["opportunity","业务机会"],["execution","已签约执行"]].map(([k,v]) => `<button class="btn ${state.opFilter === k ? "btn-primary" : "btn-ghost"}" onclick="state.opFilter='${k}'; renderProjects()">${v}</button>`).join("")}</div>
${renderTable(["项目/客户", "类型", "状态", "金额", "当前阶段", "最新跟进"], rows, clicks)}
${renderTable(["项目", "项目说明", "当前阶段", "项目金额", "负责人", "进展"], rows, items.map((x) => ({ resource: "operations", id: x.id })))}
</div>`;
}
function showTaskModal(projectId) {
const project = state.data.operations.find((x) => x.id === projectId);
const tasks = (state.data.tasks || []).filter((t) => t.project_id === projectId);
const phases = ["项目准备", "项目执行", "项目验收", "验收完毕"];
document.querySelector("#taskModal").innerHTML = `<div class="task-overlay" onclick="closeTaskModal()"><div class="task-panel" onclick="event.stopPropagation()"><div class="flex items-center justify-between border-b border-slate-200 px-6 py-4"><h2 class="text-lg font-bold">${project.project_name} · 任务清单</h2><button class="btn btn-ghost btn-sm" onclick="closeTaskModal()">关闭</button></div><div class="grid gap-4 p-6">${phases.map((phase) => {
const pt = tasks.filter((t) => t.phase === phase);
return `<div class="rounded-lg border border-slate-200"><div class="rounded-t-lg bg-slate-50 px-4 py-2.5 font-semibold text-slate-700 text-sm">${phase}${pt.length ? ` <span class="text-slate-400 font-normal">(${pt.filter(t=>t.task).length}/${pt.length})</span>` : ""}</div><div class="p-3 grid gap-2">${pt.length ? pt.map((t) => `<div class="flex items-start gap-3 rounded-md bg-white px-3 py-2 text-sm"><input type="checkbox" class="mt-0.5" ${t.task ? "" : "checked disabled"}><div class="flex-1"><p class="text-slate-800">${t.milestone ? `<strong>${t.milestone}</strong>` : ""}${t.task}</p>${t.owner ? `<p class="text-xs text-slate-400 mt-0.5">👤 ${t.owner}${t.due_date ? " · 📅 " + t.due_date : ""}</p>` : ""}</div></div>`).join("") : `<p class="text-sm text-slate-400 py-2 text-center">暂无任务</p>`}</div></div>`;
}).join("")}</div></div></div>`;
document.querySelector("#taskModal").classList.add("active");
}
window.closeTaskModal = () => document.querySelector("#taskModal").classList.remove("active");
function renderProposals() {
const proposalRows = state.data.proposals.map((p) => [p.customer_or_project_name, p.version, badge(p.status), p.files.length + " 个"]);
const proposalClicks = state.data.proposals.map((p) => ({ resource: "proposals", id: p.id }));
@@ -354,20 +341,6 @@ function openDrawer(resource, id) {
<form id="drawerForm" class="drawer-fields">${fields.map(([key,label]) => drawerField(fieldIcons[key] || "circle", label, key, item[key], multilineFields.includes(key))).join("")}</form>
</section>
${resource === "proposals" ? `<section><h3 class="drawer-section-title">方案文件</h3><div class="grid gap-2">${["方案","成本","SOP","财务流程"].map((cat) => fileGroup("proposal", item.id, item.version, cat, item.files.filter((f) => f.file_category === cat))).join("")}</div></section>` : ""}
${resource === "operations" ? (() => {
const tasks = (state.data.tasks || []).filter((t) => t.project_id === id);
if (!tasks.length) return "";
const phases = [...new Set(tasks.map((t) => t.phase))];
return `<section><h3 class="drawer-section-title">项目任务</h3><div class="grid gap-3">${phases.map((phase) => {
const pt = tasks.filter((t) => t.phase === phase);
return `<div class="rounded-md border border-slate-200 p-3"><p class="text-[13px] font-semibold text-slate-700 mb-2"><i data-lucide="layers" style="width:14px;height:14px;display:inline;vertical-align:-2px;margin-right:4px"></i>${phase}</p><div class="grid gap-1.5">${pt.map((t) => {
const due = t.due_date ? `<span class="text-[11px] text-slate-400 ml-1">📅 ${t.due_date}</span>` : "";
const owner = t.owner ? `<span class="text-[11px] text-slate-500 ml-1">👤 ${t.owner}</span>` : "";
const blocker = t.blockers ? `<p class="text-[11px] text-red-600 mt-0.5">⚠ ${t.blockers}</p>` : "";
return `<div class="rounded bg-slate-50 px-2.5 py-1.5"><p class="text-[12px] text-slate-800"><strong>${t.milestone ? t.milestone + "": ""}</strong>${t.task}${due}${owner}</p>${blocker}</div>`;
}).join("")}</div></div>`;
}).join("")}</div></section>`;
})() : ""}
${followupTarget ? `<section>
<h3 class="drawer-section-title">活动 / 跟进</h3>
<div class="grid gap-2">${(item.followups || []).map((f) => `<div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span>${f.follower} · ${f.follow_up_method}</span><span>${f.followed_at}</span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html="${encodeURIComponent(f.content || '')}"></div>${f.next_action ? `<p class="mt-1 leading-5 text-blue-700">下一步:${text(f.next_action)}</p>` : ""}</div><button class="activity-delete" onclick="deleteFollowup(event, ${f.id}, '${resource}', ${item.id})" title="删除评论"><i data-lucide="trash-2"></i></button></div>`).join("")}</div>

View File

@@ -489,3 +489,16 @@ td {
color: #64748b;
margin: 4px 0;
}
/* Task Modal */
.task-modal { display: none; }
.task-modal.active { display: block; }
.task-overlay {
position: fixed; inset: 0; background: rgba(15,23,42,0.4); z-index: 200;
display: flex; align-items: flex-start; justify-content: center;
padding-top: 60px; overflow-y: auto;
}
.task-panel {
background: white; border-radius: 12px; width: 800px; max-width: 90vw;
box-shadow: 0 20px 60px rgba(0,0,0,0.15); margin-bottom: 60px;
}