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 `
-
-
${files.length ? files.map(fileItem).join("") : `
暂无文件
`}
-
`;
-}
-
-function fileItem(file) {
- return `
`;
-}
-
-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) {
${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 @@
-
+
-