// drawer.js — 详情抽屉 + 评论 + 删除 function drawerField(icon, label, name, value, multiline = false, customControl = null) { const safeValue = esc(value || ""); const control = customControl ? customControl : multiline ? `` : ``; return `
${label}
${control}
`; } function openDrawer(resource, id) { const list = resource === "sales" ? state.data.sales : resource === "operations" ? state.data.operations : resource === "proposals" ? state.data.proposals : state.data.products; const item = list.find((x) => x.id === id); const drawer = document.querySelector("#drawer"); const fields = resource === "sales" ? [["target_customer","业务机会"],["priority","优先级"],["status","状态"]] : resource === "operations" ? [["project_name","项目名称"],["owner","负责人"],["expected_sign_date","截止时间"],["expected_contract_amount","金额"],["notes","项目说明"]] : resource === "proposals" ? [["customer_or_project_name","客户/项目"],["proposal_type","方案类型"],["notes","备注"]] : [["product_name","版本名称"],["version","版本号"],["priority","优先级"],["version_goal","版本目标"],["start_date","启动时间"],["plan_date","产品方案"],["dev_done_date","研发完成"],["test_date","测试完成"],["launch_date","上线时间"],["notes","进展备注"]]; const fieldIcons = { target_customer: "user", priority: "flag", status: "circle-dot", project_name: "briefcase-business", project_version: "git-branch", project_status: "circle-dot", current_stage: "map-pin", owner: "user", customer_need: "file-text", expected_contract_amount: "banknote", expected_sign_date: "calendar", sign_probability: "percent", sop_stage: "list-checks", execution_progress: "activity", current_deliverable: "package", risks: "alert-triangle", next_action: "arrow-right", product_name: "box", version: "tag", version_goal: "target", feature_list: "list", platform: "layers", launch_date: "calendar", notes: "sticky-note", proposal_type: "tag", customer_or_project_name: "building", priority: "flag", owner: "user", start_date: "play", plan_date: "file-text", dev_done_date: "check-square", test_date: "bug", devs: "users", testers: "shield-check" }; const multilineFields = ["customer_need", "current_deliverable", "risks", "next_action", "version_goal", "feature_list", "notes"]; const followupTarget = resource === "sales" ? "sales" : resource === "proposals" ? "proposal" : resource === "operations" ? "operation" : resource === "products" ? "product" : ""; const title = esc(item.target_customer || item.project_name || (item.customer_or_project_name ? `${item.customer_or_project_name} · ${item.proposal_type || ''}` : "") || item.product_name); const titleForAttr = esc(item.target_customer || item.project_name || (item.customer_or_project_name ? `${item.customer_or_project_name} · ${item.proposal_type || ''}` : "") || item.product_name); drawer.innerHTML = `

Detail Drawer

${title}

${resource === "products" ? (() => { const dDays = (s, e) => { if (!s || !e) return '-'; const d = Math.round((new Date(e) - new Date(s)) / 86400000); return d >= 0 ? d + ' 天' : '-'; }; return `

耗时统计

总耗时(上线−启动)

${dDays(item.start_date, item.launch_date)}

产品耗时(方案−启动)

${dDays(item.start_date, item.plan_date)}

研发耗时(研发完成−方案)

${dDays(item.plan_date, item.dev_done_date)}

测试耗时(测试完成−研发完成)

${dDays(item.dev_done_date, item.test_date)}

`; })() : ""}

属性

${resource === "operations" ? drawerField("map-pin", "当前阶段", "current_stage", "", false, ``) : ""} ${fields.map(([key,label]) => { if (resource === "products" && key === "priority") { return `
优先级
状态
`; } if (resource === "products" && (key === "start_date" || key === "plan_date" || key === "dev_done_date" || key === "test_date" || key === "launch_date")) { return drawerField("calendar", label, key, item[key], false, ``); } return drawerField(fieldIcons[key] || "circle", label, key, item[key], multilineFields.includes(key)); }).join("")}
${resource === "proposals" ? `

附件

${fileGroup("proposal", item.id, "", "附件", item.files || [])}
` : ""} ${followupTarget ? `

活动 / 跟进

${(item.followups || []).map((f) => `
${esc(f.follower)} · ${esc(f.follow_up_method)}${esc(f.followed_at)}
${f.next_action ? `

下一步:${text(f.next_action)}

` : ""}
`).join("")}
支持富文本编辑
` : ""}
`; drawer.classList.add("open"); bindDrawerAutosave(resource, item.id, item); if (window.lucide) window.lucide.createIcons(); renderUploadTasks(); drawer.querySelectorAll(".rich-content").forEach((el) => { const html = el.dataset.html; if (html) el.innerHTML = decodeURIComponent(html); }); const squireDiv = drawer.querySelector(".squire-editor"); if (squireDiv && window.Squire) { const id = squireDiv.id; if (window.squireInstances[id]) window.squireInstances[id].destroy(); const sq = new Squire(squireDiv, { blockTag: "P" }); sq.addEventListener("input", () => { const form = squireDiv.closest("form"); const btn = form.querySelector(".comment-submit"); }); window.squireInstances[id] = sq; squireDiv.addEventListener("focus", () => squireDiv.classList.add("focused")); squireDiv.addEventListener("blur", () => { if (!squireDiv.textContent.trim()) squireDiv.classList.remove("focused"); }); } } function setDrawerSaveStatus(message, tone = "muted") { const el = document.querySelector("#drawerSaveStatus"); if (!el) return; el.textContent = message; el.dataset.tone = tone; } function bindDrawerAutosave(resource, id, item) { document.querySelectorAll("#drawerForm .form-ctrl").forEach((field) => { field.addEventListener("keydown", (event) => { if (event.key === "Enter" && field.tagName !== "TEXTAREA") field.blur(); }); const doSave = async () => { const value = field.value; if (value === field.dataset.original) return; const previous = field.dataset.original; field.dataset.original = value; setDrawerSaveStatus("保存中…"); try { await api(`/api/${resource}/${id}`, { method: "PUT", body: JSON.stringify({ data: { [field.name]: value } }) }); item[field.name] = value; const titleValue = item.target_customer || item.project_name || item.customer_or_project_name || item.product_name; const titleEl = document.querySelector(".drawer-title"); if (titleEl) titleEl.textContent = titleValue; renderActive(); setDrawerSaveStatus("已保存", "success"); setTimeout(() => setDrawerSaveStatus(""), 1200); } catch (error) { field.dataset.original = previous; setDrawerSaveStatus("保存失败", "danger"); toast(`自动保存失败:${error.message}`, "error"); } }; field.addEventListener("blur", doSave); if (field.tagName === "SELECT") field.addEventListener("change", doSave); }); } window.openDrawer = openDrawer; window.closeDrawer = () => document.querySelector("#drawer").classList.remove("open"); window.deleteDrawerItem = async (resource, id) => { if (!confirm("确认删除?此操作不可撤销。")) return; try { const listKey = { sales: "sales", proposals: "proposals", operations: "operations", products: "products" }[resource]; let name = ""; if (listKey && state.data[listKey]) { const item = state.data[listKey].find(x => x.id === id); name = item ? (item.target_customer || item.project_name || item.customer_or_project_name || item.product_name || "") : ""; } await api(`/api/${resource}/${id}`, { method: "DELETE" }); if (name) { const resType = { sales: "sales", proposals: "proposal", operations: "operation", products: "product" }[resource] || resource; logActivity(resType, id, "删除了「" + name + "」"); } closeDrawer(); await load(); } catch (error) { toast("删除失败:" + error.message, "error"); } }; // Squire 富文本编辑器 window.squireInstances = {}; window.squireCmd = (cmd) => { const currentEditor = document.querySelector(".squire-editor"); if (!currentEditor) return; const id = currentEditor.id; const sq = window.squireInstances[id]; if (!sq) return; sq.focus(); setTimeout(() => { if (cmd === "bold") { sq.hasFormat("b") || sq.hasFormat("strong") ? sq.removeBold() : sq.bold(); } else if (cmd === "italic") { sq.hasFormat("i") || sq.hasFormat("em") ? sq.removeItalic() : sq.italic(); } else if (cmd === "underline") { sq.hasFormat("u") ? sq.changeFormat(null, { tag: "u" }, null) : sq.changeFormat({ tag: "u" }, null, null); } else if (cmd === "strikethrough") { sq.hasFormat("s") || sq.hasFormat("del") || sq.hasFormat("strike") ? sq.changeFormat(null, { tag: "s" }, null) : sq.changeFormat({ tag: "s" }, null, null); } else { sq[cmd](); } }, 10); }; window.submitComment = async (event, targetType, targetId, resource) => { event.preventDefault(); const form = event.currentTarget; const editorDiv = form.querySelector(".squire-editor"); const sq = window.squireInstances[editorDiv.id]; const content = sq ? sq.getHTML().trim() : ""; if (!content || content === "

" || content === "


") return; const button = form.querySelector(".comment-submit"); button.disabled = true; button.textContent = "发送中…"; await api(`/api/followups/${targetType}/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content } }) }); await load(); openDrawer(resource, targetId); }; window.deleteActivity = async (id) => { if (!confirm("确认删除这条动态?")) return; await api(`/api/followups/${id}`, { method: "DELETE" }); await load(); }; window.deleteFollowup = async (event, followupId, resource, targetId) => { event.stopPropagation(); if (!confirm("确认删除这条评论?")) return; await api(`/api/followups/${followupId}`, { method: "DELETE" }); await load(); openDrawer(resource, targetId); }; window.saveDrawerField = async (el, resource, id) => { const name = el.name; const value = el.value; // 产品日期约束 if (resource === "products") { const listKey = "products"; const product = (state.data[listKey] || []).find(x => x.id === id); if (product) { // 启动时间必填 if (name === "start_date" && !value) { toast("启动时间为必填项", "error"); el.value = product.start_date || ''; el.focus(); return; } // 其他 4 个时间不能早于启动时间 if (["plan_date","dev_done_date","test_date","launch_date"].includes(name) && value && product.start_date && value < product.start_date) { toast("该时间不能早于启动时间(" + product.start_date + ")", "error"); el.value = product[name] || ''; el.focus(); return; } } } try { await api(`/api/${resource}/${id}`, { method: "PUT", body: JSON.stringify({ data: { [name]: value } }) }); const listKey = { sales: "sales", proposals: "proposals", operations: "operations", products: "products" }[resource]; if (listKey && state.data[listKey]) { const item = state.data[listKey].find(x => x.id === id); if (item) item[name] = value; } } catch (error) { toast("保存失败:" + error.message, "error"); } };