From de86f01d6c9e31b8f2db1a132fec62b01c861f45 Mon Sep 17 00:00:00 2001 From: mac Date: Sat, 30 May 2026 01:10:21 +0800 Subject: [PATCH] =?UTF-8?q?v1.0.0:=20=E9=A6=96=E6=AC=A1=E6=AD=A3=E5=BC=8F?= =?UTF-8?q?=E5=8F=91=E5=B8=83=20=E2=80=94=20Squire=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E3=80=81Plane=20=E9=A3=8E=E6=A0=BC=E6=8A=BD=E5=B1=89?= =?UTF-8?q?=E3=80=81=E9=A6=96=E9=A1=B5=E5=B8=83=E5=B1=80=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION_LOG.md | 45 +++++++++++----- data/opc.sqlite | Bin 0 -> 40960 bytes static/app.js | 112 +++++++++++++++++++++++++++++++++------- static/styles.css | 120 ++++++++++++++++++++++++++++++------------- templates/index.html | 3 +- 5 files changed, 212 insertions(+), 68 deletions(-) create mode 100644 data/opc.sqlite 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 0000000000000000000000000000000000000000..0684797db32c69b7a570704937ecbe9e60054c16 GIT binary patch literal 40960 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 tVPa(%VJDZI%E4 literal 0 HcmV?d00001 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 @@ - + -