diff --git a/VERSION_LOG.md b/VERSION_LOG.md index 5912f8d..d2e2627 100644 --- a/VERSION_LOG.md +++ b/VERSION_LOG.md @@ -1,18 +1,37 @@ # OPC Manager Version Log -## opc-manager-v0.1.0 - 2026-05-30 +## v1.0.0 — 2026-05-30 -- Deployed the Flask/Jinja OPC workbench to the business server. -- Runtime path: `/opt/opc-manager`. -- Runtime service: `opc-manager.service` managed by systemd. -- Runtime command: `gunicorn -w 2 -b 127.0.0.1:5177 backend.flask_app:app`. -- Public URL: `https://opc.yxcowork.vip`. -- Health check: `https://opc.yxcowork.vip/api/health`. -- Database path: `/opt/opc-manager/data/opc.sqlite`. -- Caddy route: `opc.yxcowork.vip -> localhost:5177`. +**首次正式发布** -Deployment rule from this version onward: +### Features +- 首页概览:7 项关键指标卡片(4 列自动换行)、财务趋势图、风险提醒、近期动态 +- 销售管理:客户表格 + 抽屉详情(字段失焦自动保存) +- 业务方案:版本表格 + 抽屉(文件上传/预览/下载/删除) +- 运营管理:项目表格(业务机会/已签约执行分类筛选)+ 抽屉 +- 产品研发:版本表格 + 抽屉 +- 财务管理:月度收入/毛利/成本/净利曲线图 + 明细表 -- Every deployment must be committed to Git. -- Every deployment must create a corresponding Git tag. -- Every deployment must update this version log. +### Interactions +- 所有抽屉:Plane 风格紧凑布局(720px)、字段失焦自动保存、状态指示 +- 评论区:Squire 富文本编辑器(加粗/斜体/下划线/删除线/无序列表/有序列表/引用/撤销/重做) +- 评论支持删除,带确认弹窗 +- 评论内容保留 HTML 格式(加粗、列表等) +- 图标库:Lucide + +### Tech Stack +- Backend: Flask + SQLite +- Frontend: Vanilla JS + Tailwind CSS CDN +- Editor: Squire (Fastmail) +- Charts: Chart.js +- Icons: Lucide + +### Fixes +- 首页财务图表空白问题:固定容器高度 140px + maintainAspectRatio: false +- 首页指标卡片布局:grid-cols-7 → grid-cols-4 自动换行 +- 风险提醒文字竖排:grid-cols-2 等宽布局 + break-words +- 评论区工具栏按钮无效:onclick → onmousedown 防止焦点丢失 +- 格式 toggle 无效:hasFormat 检测 + removeBold/removeItalic +- 列表按钮无效:Squire API 替代 Trix +- 评论内容格式丢失:encodeURIComponent 编码 + decodeURIComponent 渲染 +- 列表显示无标记:list-style: revert 覆盖 Tailwind reset diff --git a/data/opc.sqlite b/data/opc.sqlite new file mode 100644 index 0000000..0684797 Binary files /dev/null and b/data/opc.sqlite differ diff --git a/static/app.js b/static/app.js index 7d1c228..88e5acb 100644 --- a/static/app.js +++ b/static/app.js @@ -65,6 +65,28 @@ function render() { renderProducts(); renderFinance(); if (window.lucide) window.lucide.createIcons(); + // Decode and render rich HTML content in followup records + drawer.querySelectorAll(".rich-content").forEach((el) => { + const html = el.dataset.html; + if (html) el.innerHTML = decodeURIComponent(html); + }); + // Initialize Squire editor + 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; + // Handle placeholder + squireDiv.addEventListener("focus", () => squireDiv.classList.add("focused")); + squireDiv.addEventListener("blur", () => { + if (!squireDiv.textContent.trim()) squireDiv.classList.remove("focused"); + }); + } } function renderHome() { @@ -72,7 +94,7 @@ function renderHome() { const m = summary.metrics; document.querySelector("#home").innerHTML = `
-
+
${[ ["P0 客户数", m.p0_customers, "sales"], ["跟进中销售机会", m.active_sales, "sales"], @@ -83,11 +105,11 @@ function renderHome() { ["即将上线版本", m.upcoming_products, "products"], ].map(([label, value, tab]) => ``).join("")}
-
- ${card(`

财务趋势

${badge("YYYY-MM")}
`, "p-5")} - ${card(`

风险提醒

${(summary.risks.length ? summary.risks : [{ title: "暂无高风险", content: "当前无明确阻塞,按周更新即可。" }]).map((r) => `

${r.title}

${r.content}

`).join("")}
`, "p-5")} +
+ ${card(`

财务趋势

${badge("YYYY-MM")}
`, "p-4")} + ${card(`

风险提醒

${(summary.risks.length ? summary.risks : [{ title: "暂无高风险", content: "当前无明确阻塞,按周更新即可。" }]).map((r) => `

${r.title}

${r.content}

`).join("")}
`, "p-5")}
- ${card(`

近期动态

${summary.recent.map((r) => `
${r.content}${r.followed_at}
`).join("")}
`, "p-5")} + ${card(`

近期动态

${summary.recent.map((r) => `
${r.content}${r.followed_at}
`).join("")}
`, "p-5")}
`; renderChart(financeMonthly); @@ -108,7 +130,7 @@ function renderChart(data) { { label: "净利", data: data.map((x) => x.net_profit), borderColor: "#7c3aed", tension: 0.3 }, ], }, - options: { responsive: true, plugins: { legend: { position: "bottom" } } }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 11 } }, grid: { display: false } }, y: { ticks: { font: { size: 11 } } } } }, }); } @@ -250,7 +272,7 @@ function renderChartOn(id, data) { { label: "净利", data: data.map((x) => x.net_profit), borderColor: "#7c3aed", tension: 0.3 }, ], }, - options: { responsive: true, plugins: { legend: { position: "bottom" } } }, + options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 11 } }, grid: { display: false } }, y: { ticks: { font: { size: 11 } } } } }, }); } @@ -296,12 +318,24 @@ function openDrawer(resource, id) { ${resource === "proposals" ? `

方案文件

${["方案","成本","SOP","财务流程"].map((cat) => fileGroup("proposal", item.id, item.version, cat, item.files.filter((f) => f.file_category === cat))).join("")}
` : ""} ${followupTarget ? `

活动 / 跟进

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

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

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

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

` : ""}
`).join("")}
- - +
+ + + + + + + + + + + +
+
- 支持 Markdown 格式 + 支持富文本编辑
@@ -310,6 +344,28 @@ function openDrawer(resource, id) { drawer.classList.add("open"); bindDrawerAutosave(resource, item.id, item); if (window.lucide) window.lucide.createIcons(); + // Decode and render rich HTML content in followup records + drawer.querySelectorAll(".rich-content").forEach((el) => { + const html = el.dataset.html; + if (html) el.innerHTML = decodeURIComponent(html); + }); + // Initialize Squire editor + 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; + // Handle placeholder + squireDiv.addEventListener("focus", () => squireDiv.classList.add("focused")); + squireDiv.addEventListener("blur", () => { + if (!squireDiv.textContent.trim()) squireDiv.classList.remove("focused"); + }); + } } function setDrawerSaveStatus(message, tone = "muted") { @@ -352,12 +408,36 @@ function bindDrawerAutosave(resource, id, item) { window.openDrawer = openDrawer; window.closeDrawer = () => document.querySelector("#drawer").classList.remove("open"); +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 editor = form.querySelector("trix-editor"); - const content = editor.editor.getDocument().toString().trim(); - if (!content) return; + 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 = "发送中…"; @@ -374,10 +454,6 @@ window.deleteFollowup = async (event, followupId, resource, targetId) => { openDrawer(resource, targetId); }; -document.querySelector("#drawer").addEventListener("click", (event) => { - if (event.target === event.currentTarget) closeDrawer(); -}); - document.querySelector("#tabs").addEventListener("click", (event) => { const button = event.target.closest("button[data-tab]"); if (button) switchTab(button.dataset.tab); diff --git a/static/styles.css b/static/styles.css index 2d3d984..014fb65 100644 --- a/static/styles.css +++ b/static/styles.css @@ -157,13 +157,14 @@ td { box-shadow: -18px 0 45px rgba(15, 23, 42, 0.14); height: 100vh; overflow-y: auto; - width: 560px; + width: 720px; } .file-link { color: #1d4ed8; font-size: 12px; font-weight: 700; + white-space: nowrap; } .drawer-section-title { @@ -313,14 +314,50 @@ td { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.08); } -/* Trix editor inside comment box */ -.comment-trix { - min-height: 80px; +/* Squire editor */ +.squire-toolbar { + align-items: center; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; + display: flex; + gap: 2px; + padding: 5px 6px; } -.comment-trix trix-editor { +.squire-btn { + align-items: center; + background: transparent; + border: none; + border-radius: 4px; + color: #475569; + cursor: pointer; + display: inline-flex; + height: 28px; + justify-content: center; + transition: background 0.15s; + width: 28px; +} + +.squire-btn:hover { + background: #e2e8f0; + color: #0f172a; +} + +.squire-btn [data-lucide] { + height: 15px; + width: 15px; +} + +.squire-sep { + background: #e2e8f0; + display: inline-block; + height: 18px; + margin: 0 2px; + width: 1px; +} + +.squire-editor { border: 0; - border-radius: 0; font-size: 13px; line-height: 1.55; min-height: 80px; @@ -328,30 +365,32 @@ td { outline: none; } -.comment-trix trix-toolbar { - border: 0; - border-top: 1px solid #e2e8f0; - background: #f8fafc; - padding: 6px 6px; +.squire-editor p { + margin: 0; + min-height: 1em; } -.comment-trix trix-toolbar .trix-button { - border-radius: 4px; - padding: 3px 5px; - font-size: 12px; - background: transparent; - border-color: transparent; +.squire-editor ul, +.squire-editor ol { + list-style: revert; + padding-left: 24px; + margin: 4px 0; +} +.squire-editor ul { + list-style-type: disc; +} +.squire-editor ol { + list-style-type: decimal; +} +.squire-editor li { + margin: 2px 0; } -.comment-trix trix-toolbar .trix-button:hover, -.comment-trix trix-toolbar .trix-button.trix-active { - background: #e2e8f0; - color: #1e293b; -} - -.comment-trix trix-toolbar .trix-button-group { - border-color: #e2e8f0; - margin-right: 4px; +.squire-editor blockquote { + border-left: 3px solid #e2e8f0; + color: #64748b; + margin: 4px 0; + padding-left: 10px; } .comment-toolbar { @@ -414,26 +453,37 @@ td { width: 14px; } /* Trix content in activity items */ -.activity-item .trix-content { +.activity-item .rich-content { font-size: 13px; line-height: 1.55; } -.activity-item .trix-content div { +.activity-item .rich-content div { font-size: 13px; } -.activity-item .trix-content strong { +.activity-item .rich-content strong { font-weight: 600; } -.activity-item .trix-content a { +.activity-item .rich-content a { color: #1d4ed8; text-decoration: underline; } -.activity-item .trix-content ul, -.activity-item .trix-content ol { - padding-left: 16px; - margin: 4px 0; +.activity-item .rich-content ul, +.activity-item .rich-content ol { + padding-left: 24px; + list-style: revert; } -.activity-item .trix-content blockquote { +.activity-item .rich-content ul { + list-style-type: disc; +} +.activity-item .rich-content ol { + list-style-type: decimal; +} +.activity-item .rich-content ul, +.activity-item .rich-content ol, +.activity-item .rich-content li { + margin: 2px 0; +} +.activity-item .rich-content blockquote { border-left: 3px solid #e2e8f0; padding-left: 10px; color: #64748b; diff --git a/templates/index.html b/templates/index.html index f5dbfa7..cce25da 100644 --- a/templates/index.html +++ b/templates/index.html @@ -22,9 +22,8 @@ - + -