From 301dfd0dfb8b49de6899d4b454e0fec5b467ead7 Mon Sep 17 00:00:00 2001 From: mac Date: Mon, 15 Jun 2026 10:01:31 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.0=20=E2=80=94=20=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=9C=BA=E4=BC=9A+=E8=BF=90=E8=90=A5?= =?UTF-8?q?=E4=B8=BA=E9=87=8D=E7=82=B9=E9=A1=B9=E7=9B=AE=20Tab=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=A1=B9=E7=9B=AE=E4=BB=BB=E5=8A=A1=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION_LOG.md | 5 ++ backend/flask_app.py | 35 ++++++++++++- static/app.js | 122 +++++++++++++++++++------------------------ templates/index.html | 6 +-- 4 files changed, 95 insertions(+), 73 deletions(-) diff --git a/VERSION_LOG.md b/VERSION_LOG.md index ea91c58..90dd98e 100644 --- a/VERSION_LOG.md +++ b/VERSION_LOG.md @@ -1,5 +1,10 @@ # OPC Manager Version Log +## v1.2.0 — 2026-06-15 +- 业务机会 + 运营管理合并为「重点项目」Tab,统一表格展示 +- 新增项目任务追踪:按阶段分组展示里程碑/执行项/负责人/截止日/卡点 +- 新增 `project_tasks` 表,抽屉内展示项目时间线 + ## v1.1.0 — 2026-06-15 - 首页指标升级:增加已签约合同总额、合同流程中金额、年度/Q2 累计确收、年度/Q2 累计毛利、已签约未执行 - 运营表格增加「金额」列 diff --git a/backend/flask_app.py b/backend/flask_app.py index 2ccc9f9..9b8d96f 100644 --- a/backend/flask_app.py +++ b/backend/flask_app.py @@ -141,6 +141,19 @@ CREATE TABLE IF NOT EXISTS file_assets ( notes TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE IF NOT EXISTS project_tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id INTEGER NOT NULL, + phase TEXT NOT NULL DEFAULT '', + milestone TEXT NOT NULL DEFAULT '', + task TEXT NOT NULL DEFAULT '', + owner TEXT NOT NULL DEFAULT '', + due_date TEXT NOT NULL DEFAULT '', + blockers TEXT NOT NULL DEFAULT '', + notes TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); """ ) @@ -242,6 +255,25 @@ CREATE TABLE IF NOT EXISTS file_assets ( (month, record_type, category, amount, f"{month}-01", notes), ) + # Seed project tasks for 信达科普文章项目 (project_id=1) + tasks_seed = [ + ("阶段1:渠道与商务确认", "商务对接", "合同签订", "Anna", "2026-06-30", "法务审核中", "合同签订后开始执行"), + ("阶段1:渠道与商务确认", "官媒渠道确认", "沟通官媒确定", "段丽华", "2026-06-30", "官媒尽力推,以先达成合作为准", "集团支持"), + ("阶段1:渠道与商务确认", "官媒渠道确认", "官媒合作签约", "段丽华", "2026-06-18", "", "官媒确认细节"), + ("阶段2:系统与标准搭建", "系统开发上线", "音频专访系统开发上线", "戴敏/梁军营", "2026-06-18", "客户比较着急执行,需要技术的资源", ""), + ("阶段2:系统与标准搭建", "系统开发上线", "精品视频系统开发上线", "戴敏/梁军营", "2026-06-25", "", ""), + ("阶段2:系统与标准搭建", "标准与培训", "业务执行手册SOP", "胡龙飞", "2026-06-12", "", "系统开发上线"), + ("阶段3:人员与审核入驻", "团队组建", "医学审核人员到位", "胡龙飞", "2026-06-15", "", "审核人员招聘"), + ("阶段3:人员与审核入驻", "团队组建", "视频制作人员到位", "胡龙飞", "2026-06-18", "", "项目经理招聘"), + ("阶段4:供应链与制作", "供应商准入", "准入拍摄/剪辑/主持人", "胡龙飞/侯亚凤", "2026-06-18", "", ""), + ("阶段2:系统与标准搭建", "脚本生产及审核", "生产脚本", "军营", "2026-06-12", "脚本目前生产比较机械,需要提前准备", "细分标签领域完成"), + ] + for phase, milestone, task, owner, due_date, blockers, notes in tasks_seed: + conn.execute( + "INSERT INTO project_tasks (project_id,phase,milestone,task,owner,due_date,blockers,notes) VALUES (?,?,?,?,?,?,?,?)", + (1, phase, milestone, task, owner, due_date, blockers, notes), + ) + conn.commit() conn.close() @@ -346,7 +378,7 @@ def bootstrap(): "recent": rows(conn, "SELECT * FROM follow_up_records ORDER BY id DESC LIMIT 8"), "risks": [{"title": "执行提醒", "content": x["next_action"]} for x in operations if x["next_action"]][:5], } - return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "financeMonthly": monthly_finance(conn)}) + return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "financeMonthly": monthly_finance(conn), "tasks": rows(conn, "SELECT * FROM project_tasks ORDER BY phase, id")}) finally: conn.close() @@ -357,6 +389,7 @@ TABLES = { "operations": ("operation_projects", ["project_name", "project_version", "project_type", "project_status", "current_stage", "owner", "target_customer", "customer_need", "expected_contract_amount", "expected_sign_date", "sign_probability", "next_action", "sop_stage", "execution_progress", "current_deliverable", "risks", "notes"]), "products": ("product_versions", ["product_name", "version", "version_goal", "feature_list", "launch_date", "status", "platform", "notes"]), "finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes"]), + "tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes"]), } diff --git a/static/app.js b/static/app.js index 274c68b..8f02565 100644 --- a/static/app.js +++ b/static/app.js @@ -61,9 +61,8 @@ function switchTab(tab) { function render() { if (!state.data) return; renderHome(); - renderSales(); + renderProjects(); renderProposals(); - renderOperations(); renderProducts(); renderFinance(); if (window.lucide) window.lucide.createIcons(); @@ -98,10 +97,10 @@ function renderHome() {
${[ - ["P0 客户数", m.p0_customers, "sales"], - ["跟进中销售机会", m.active_sales, "sales"], - ["已签约执行项目", m.execution_projects, "operations"], - ["有风险项目", m.risk_projects, "operations"], + ["P0 客户数", m.p0_customers, "projects"], + ["跟进中销售机会", m.active_sales, "projects"], + ["已签约执行项目", m.execution_projects, "projects"], + ["有风险项目", m.risk_projects, "projects"], ["本月收入", money(m.monthly_revenue), "finance"], ["本月净利", money(m.monthly_net_profit), "finance"], ["即将上线版本", m.upcoming_products, "products"], @@ -110,8 +109,8 @@ function renderHome() {
${[ - ["已签约合同总额", money(m.signed_amount), "operations"], - ["合同流程中", money(m.pipeline_amount), "operations"], + ["已签约合同总额", money(m.signed_amount), "projects"], + ["合同流程中", money(m.pipeline_amount), "projects"], ["年度累计确收", money(m.revenue_annual), "finance"], ["Q2 累计确收", money(m.revenue_q2), "finance"], ["年度累计毛利", money(m.gross_annual), "finance"], @@ -174,77 +173,50 @@ window.createProduct = (event) => createResource(event, "products"); window.createFinance = (event) => createResource(event, "finance"); window.switchTab = switchTab; -function renderSales() { - const rows = state.data.sales.map((x) => [x.target_customer, badge(x.priority), badge(x.status), text(x.latest_follow_up_record)]); - const salesClicks = state.data.sales.map((x) => ({ resource: "sales", id: x.id })); - document.querySelector("#sales").innerHTML = `
+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) => [`${x.name}${x.version ? `

${x.version}

` : ""}`, 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 })); + document.querySelector("#projects").innerHTML = `
${card(formHtml([ { label: "业务机会", input: `` }, { label: "优先级", input: `` }, { label: "状态", input: `` }, ], { handler: "createSales", text: `新增业务机会` }), "p-4")} - ${renderTable(["业务机会", "优先级", "状态", "最新跟进记录"], rows, salesClicks)} -
`; -} - -function renderProposals() { - const categories = ["方案", "成本", "SOP", "财务流程"]; - 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 })); - document.querySelector("#proposals").innerHTML = `
- ${card(formHtml([ - { label: "客户/项目", input: `` }, - { label: "版本号", input: `` }, - { label: "状态", input: `` }, - ], { handler: "createProposal", text: "新增版本" }), "p-4")} - ${renderTable(["客户/项目", "版本号", "状态", "文件数"], proposalRows, proposalClicks)} -
`; -} - -function fileGroup(module, ownerId, version, category, files) { - return `
-

${category}

-
${files.length ? files.map(fileItem).join("") : `

暂无文件

`}
-
`; -} - -function fileItem(file) { - return `

${file.file_name}

`; -} - -window.deleteFile = async (fileId) => { - if (!confirm("确认删除此文件?")) return; - await api(`/api/files/${fileId}`, { method: "DELETE" }); - await load(); - closeDrawer(); -}; - -window.uploadFile = async (event, module, ownerId, version, category) => { - const file = event.target.files[0]; - if (!file) return; - const form = new FormData(); - form.append("module", module); - form.append("owner_id", ownerId); - form.append("owner_version", version); - form.append("file_category", category); - form.append("file", file); - await api("/api/files/upload", { method: "POST", body: form }); - await load(); -}; - -function renderOperations() { - const items = state.opFilter === "all" ? state.data.operations : state.data.operations.filter((x) => x.project_type === state.opFilter); - const opRows = items.map((x) => [`${x.project_name}

${x.project_version}

`, badge(x.project_type), badge(x.project_status), x.expected_contract_amount ? money(x.expected_contract_amount) : "—", text(x.current_stage || x.sop_stage), `${x.files.length} 个`, text(x.latest_follow_up_record)]); - const opClicks = items.map((x) => ({ resource: "operations", id: x.id })); - document.querySelector("#operations").innerHTML = `
${card(formHtml([ { label: "项目名称", input: `` }, { label: "项目版本", input: `` }, { label: "项目类型", input: `` }, { label: "状态", input: `` }, ], { handler: "createOperation", text: "新增项目" }), "p-4")} -
${[["all","全部项目"],["opportunity","业务机会项目"],["execution","已签约执行项目"]].map(([k,v]) => ``).join("")}
- ${renderTable(["项目名称", "类型", "状态", "金额", "当前阶段", "交付文件", "最新跟进"], opRows, opClicks)} +
${[["all","全部"],["opportunity","业务机会"],["execution","已签约执行"]].map(([k,v]) => ``).join("")}
+ ${renderTable(["项目/客户", "类型", "状态", "金额", "当前阶段", "最新跟进"], rows, clicks)}
`; } @@ -338,6 +310,20 @@ function openDrawer(resource, id) {
${fields.map(([key,label]) => drawerField(fieldIcons[key] || "circle", label, key, item[key], multilineFields.includes(key))).join("")}
${resource === "proposals" ? `

方案文件

${["方案","成本","SOP","财务流程"].map((cat) => fileGroup("proposal", item.id, item.version, cat, item.files.filter((f) => f.file_category === cat))).join("")}
` : ""} + ${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 `

项目任务

${phases.map((phase) => { + const pt = tasks.filter((t) => t.phase === phase); + return `

${phase}

${pt.map((t) => { + const due = t.due_date ? `📅 ${t.due_date}` : ""; + const owner = t.owner ? `👤 ${t.owner}` : ""; + const blocker = t.blockers ? `

⚠ ${t.blockers}

` : ""; + return `

${t.milestone ? t.milestone + ":": ""}${t.task}${due}${owner}

${blocker}
`; + }).join("")}
`; + }).join("")}
`; + })() : ""} ${followupTarget ? `

活动 / 跟进

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

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

` : ""}
`).join("")}
diff --git a/templates/index.html b/templates/index.html index 2ea0e33..37595b7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -36,18 +36,16 @@
-
+
-