Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9930727d14 | ||
|
|
c5180cafbd | ||
|
|
256767ff57 | ||
|
|
7009b7fe48 | ||
|
|
268e6d4f80 | ||
|
|
50358e116d | ||
|
|
1112827a19 | ||
|
|
1c3b487caa |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ data/uploads/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
data/opc.sqlite
|
||||
|
||||
@@ -1,18 +1,65 @@
|
||||
# OPC Manager Version Log
|
||||
|
||||
## opc-manager-v0.1.0 - 2026-05-30
|
||||
## v1.1.0 — 2026-06-15
|
||||
- 首页指标升级:增加已签约合同总额、合同流程中金额、年度/Q2 累计确收、年度/Q2 累计毛利、已签约未执行
|
||||
- 运营表格增加「金额」列
|
||||
- 产品研发增加「平台」字段(真研/科普/关爱),支持平台筛选
|
||||
|
||||
- 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`.
|
||||
## v1.0.7 — 2026-06-04
|
||||
- 修复新增表单 async 后 `event.currentTarget` 丢失导致页面不刷新(影响所有新增按钮)
|
||||
- `createResource` 改用预存 form 引用 + try/catch 错误提示
|
||||
|
||||
Deployment rule from this version onward:
|
||||
## v1.0.6 — 2026-06-04
|
||||
- 修复财务 Tab Chart 无限堆积:renderChartOn 缺少旧 chart 销毁 + state 跟踪
|
||||
- 财务图表容器加固定高度(300px),避免 resize 循环
|
||||
|
||||
- Every deployment must be committed to Git.
|
||||
- Every deployment must create a corresponding Git tag.
|
||||
- Every deployment must update this version log.
|
||||
## v1.0.5 — 2026-06-04
|
||||
- "销售管理" Tab 改为"业务机会","目标客户"字段统一改为"业务机会"
|
||||
|
||||
## v1.0.4 — 2026-06-04
|
||||
- CDN 全量本地化:Tailwind / Chart.js / Squire / Lucide 下载到 `static/vendor/`,不再依赖外部 CDN
|
||||
|
||||
## v1.0.3 — 2026-06-04
|
||||
- CDN 脚本加 `defer` + `preconnect`:解决 Chart.js / Squire / Lucide 同步加载阻塞页面渲染
|
||||
|
||||
## v1.0.2 — 2026-05-30
|
||||
- 新增 Codex Skill:`opc-manager`,说"打开 OPC 工作台"即可在任意上下文启动
|
||||
|
||||
## v1.0.1 — 2026-05-30
|
||||
- 将 data/opc.sqlite 加入 .gitignore,避免运行时数据库被误提交
|
||||
|
||||
## v1.0.0 — 2026-05-30
|
||||
|
||||
**首次正式发布**
|
||||
|
||||
### Features
|
||||
- 首页概览:7 项关键指标卡片(4 列自动换行)、财务趋势图、风险提醒、近期动态
|
||||
- 销售管理:客户表格 + 抽屉详情(字段失焦自动保存)
|
||||
- 业务方案:版本表格 + 抽屉(文件上传/预览/下载/删除)
|
||||
- 运营管理:项目表格(业务机会/已签约执行分类筛选)+ 抽屉
|
||||
- 产品研发:版本表格 + 抽屉
|
||||
- 财务管理:月度收入/毛利/成本/净利曲线图 + 明细表
|
||||
|
||||
### 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
|
||||
|
||||
@@ -144,6 +144,10 @@ CREATE TABLE IF NOT EXISTS file_assets (
|
||||
);
|
||||
"""
|
||||
)
|
||||
# Schema migrations
|
||||
try: conn.execute("ALTER TABLE product_versions ADD COLUMN platform TEXT NOT NULL DEFAULT ''")
|
||||
except: pass
|
||||
|
||||
if one(conn, "SELECT id FROM sales_leads LIMIT 1"):
|
||||
conn.close()
|
||||
return
|
||||
@@ -210,15 +214,15 @@ CREATE TABLE IF NOT EXISTS file_assets (
|
||||
add_file_index(conn, "operation", project_id, version, category, op_dir / filename, external=True)
|
||||
|
||||
products = [
|
||||
("妙手医生服务小程序", "v1.1", "视频任务增强 + 积分商城", "草稿箱、批量上传、积分商城、消息通知", "2026-Q3", "规划中"),
|
||||
("数字化营销后台管理系统", "v1.2", "运营数据看板 + 智能审核", "医生活跃、任务完成率、AI 预审、渠道数据上报", "2026-Q3", "设计中"),
|
||||
("妙手患者服务", "v0.5", "科普浏览 + 医生主页 MVP", "科普文章/视频浏览、医生主页、搜索", "2026-Q3", "规划中"),
|
||||
("数字人内容平台", "v0.1", "基础数字人视频生成 MVP", "预设形象、AI 配音、脚本驱动、简单模板", "2026-Q3", "规划中"),
|
||||
("渠道分发引擎", "v1.0", "六渠道统一分发", "分发 API、内容适配、分发排期、效果追踪", "2027-Q1", "规划中"),
|
||||
("妙手医生服务小程序", "v1.1", "视频任务增强 + 积分商城", "草稿箱、批量上传、积分商城、消息通知", "2026-Q3", "规划中", "科普平台"),
|
||||
("数字化营销后台管理系统", "v1.2", "运营数据看板 + 智能审核", "医生活跃、任务完成率、AI 预审、渠道数据上报", "2026-Q3", "设计中", "真研平台"),
|
||||
("妙手患者服务", "v0.5", "科普浏览 + 医生主页 MVP", "科普文章/视频浏览、医生主页、搜索", "2026-Q3", "规划中", "科普平台"),
|
||||
("数字人内容平台", "v0.1", "基础数字人视频生成 MVP", "预设形象、AI 配音、脚本驱动、简单模板", "2026-Q3", "规划中", "科普平台"),
|
||||
("渠道分发引擎", "v1.0", "六渠道统一分发", "分发 API、内容适配、分发排期、效果追踪", "2027-Q1", "规划中", "科普平台"),
|
||||
]
|
||||
for product in products:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO product_versions (product_name,version,version_goal,feature_list,launch_date,status) VALUES (?,?,?,?,?,?)",
|
||||
"INSERT INTO product_versions (product_name,version,version_goal,feature_list,launch_date,status,platform) VALUES (?,?,?,?,?,?,?)",
|
||||
product,
|
||||
)
|
||||
conn.execute(
|
||||
@@ -304,9 +308,22 @@ def bootstrap():
|
||||
operations = attach_common(conn, "operations", rows(conn, "SELECT * FROM operation_projects ORDER BY id DESC"))
|
||||
products = attach_common(conn, "products", rows(conn, "SELECT * FROM product_versions ORDER BY id DESC"))
|
||||
finance = rows(conn, "SELECT * FROM finance_records ORDER BY month DESC, id DESC")
|
||||
current_month = "2026-05"
|
||||
revenue = sum(x["amount"] for x in finance if x["month"] == current_month and x["record_type"] == "revenue")
|
||||
cost = sum(x["amount"] for x in finance if x["month"] == current_month and x["record_type"] == "cost_expense")
|
||||
current_month = "2026-06"
|
||||
# Finance aggregates
|
||||
def sum_finance(months, rtype):
|
||||
return sum(x["amount"] for x in finance if x["month"] in months and x["record_type"] == rtype)
|
||||
months_2026 = [f"2026-{m:02d}" for m in range(1,7)]
|
||||
months_q2 = ["2026-04","2026-05","2026-06"]
|
||||
revenue_annual = sum_finance(months_2026, "revenue")
|
||||
cost_annual = sum_finance(months_2026, "cost_expense")
|
||||
revenue_q2 = sum_finance(months_q2, "revenue")
|
||||
cost_q2 = sum_finance(months_q2, "cost_expense")
|
||||
revenue_month = sum_finance([current_month], "revenue")
|
||||
cost_month = sum_finance([current_month], "cost_expense")
|
||||
# Contract aggregates
|
||||
signed_amount = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_status"] == "已签约")
|
||||
pipeline_amount = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_status"] not in ["已签约","已丢单","已归档","已完成"])
|
||||
signed_not_executed = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_type"] == "execution" and x["execution_progress"] < 100)
|
||||
summary = {
|
||||
"project_name": "科普(慰心斋)",
|
||||
"metrics": {
|
||||
@@ -314,9 +331,17 @@ def bootstrap():
|
||||
"active_sales": len([x for x in sales if x["status"] in ["待跟进", "跟进中", "方案中", "商务谈判"]]),
|
||||
"execution_projects": len([x for x in operations if x["project_type"] == "execution"]),
|
||||
"risk_projects": len([x for x in operations if x["project_status"] == "有风险" or x["risks"]]),
|
||||
"monthly_revenue": revenue,
|
||||
"monthly_net_profit": revenue - cost,
|
||||
"monthly_revenue": revenue_month,
|
||||
"monthly_net_profit": revenue_month - cost_month,
|
||||
"upcoming_products": len([x for x in products if x["status"] in ["规划中", "设计中", "开发中", "测试中"]]),
|
||||
# Extended finance metrics
|
||||
"signed_amount": signed_amount,
|
||||
"pipeline_amount": pipeline_amount,
|
||||
"revenue_annual": revenue_annual,
|
||||
"revenue_q2": revenue_q2,
|
||||
"gross_annual": revenue_annual - cost_annual,
|
||||
"gross_q2": revenue_q2 - cost_q2,
|
||||
"signed_not_executed": signed_not_executed,
|
||||
},
|
||||
"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],
|
||||
@@ -330,7 +355,7 @@ TABLES = {
|
||||
"sales": ("sales_leads", ["target_customer", "priority", "status"]),
|
||||
"proposals": ("business_proposals", ["customer_or_project_name", "version", "description", "status", "created_date"]),
|
||||
"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", "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"]),
|
||||
}
|
||||
|
||||
|
||||
67
docs/prd/CHANGELOG.md
Normal file
67
docs/prd/CHANGELOG.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# OPC 管理系统 PRD 修改日志
|
||||
|
||||
---
|
||||
|
||||
## v0.9 → v1.0(2026-06-01)
|
||||
|
||||
### 版本升级
|
||||
- PRD 阶段从「需求核对稿」升级为「实际落地版」
|
||||
- 版本号从 v0.9 → v1.0
|
||||
- 匹配 OPC-Manager v1.0.2 代码实现
|
||||
|
||||
### 功能布局变更
|
||||
|
||||
| 模块 | v0.9 | v1.0 | 原因 |
|
||||
|------|------|------|------|
|
||||
| 业务方案 Tab | 一个版本一个**卡片** | 表格布局 | 与销售、运营保持一致的交互模式,表格更适合多版本扫描 |
|
||||
| 产品 Tab | 一个版本一个**卡片** | 表格布局 | 同上,统一交互体验 |
|
||||
|
||||
### 交互细节补充
|
||||
|
||||
| 项 | v0.9 | v1.0 |
|
||||
|----|------|------|
|
||||
| 抽屉宽度 | 未指定 | 720px 固定宽度 |
|
||||
| 保存方式 | "关闭抽屉时提示保存" | 字段失焦自动保存 + 状态反馈(保存中/已保存/失败) |
|
||||
| 抽屉外点击关闭 | 未明确 | 已删除此交互 |
|
||||
| 富文本编辑器 | 未指定 | Squire RTE v1(Fastmail) |
|
||||
| 评论格式 | 未指定 | HTML 格式(encodeURIComponent 存储,decodeURIComponent 渲染) |
|
||||
| 评论删除 | 未指定 | 所有评论 hover 显示删除图标,带确认弹窗 |
|
||||
| Squire 工具栏 | 未指定 | 使用 onmousedown 防止编辑器失焦 |
|
||||
| 列表渲染 | 未指定 | .rich-content ul/ol 使用 list-style: revert 覆盖 Tailwind reset |
|
||||
|
||||
### 数据模型变更
|
||||
|
||||
| 项 | v0.9 | v1.0 |
|
||||
|----|------|------|
|
||||
| FileAsset | operation_files 独立表 | 统一 file_assets 表,通过 module 字段区分 proposal/operation |
|
||||
| FileAsset.is_external | 无 | 新增字段,标记原始文件索引(1)vs 本地上传(0) |
|
||||
| FollowUpRecord.content | 文本 | HTML 格式 |
|
||||
|
||||
### 技术栈补充
|
||||
|
||||
| 项 | v0.9 | v1.0 |
|
||||
|----|------|------|
|
||||
| 富文本编辑器 | 未指定 | Squire RTE(squire-rte CDN v1,build/squire-raw.js) |
|
||||
| Chart.js 版本 | 未指定 | 4.4.8 |
|
||||
| Chart 配置 | 未指定 | maintainAspectRatio: false + 固定容器高度 140px |
|
||||
| Flask 版本 | 未指定 | 3.1.3 |
|
||||
| 项目结构 | 未列出 | 新增 7.6 节完整目录结构 |
|
||||
|
||||
### 章节结构变更
|
||||
|
||||
| 项 | v0.9 | v1.0 |
|
||||
|----|------|------|
|
||||
| 首页指标 | 6 项 | 7 项(新增"即将上线版本数") |
|
||||
| 首页指标布局 | 未指定 | 4 列网格(grid-cols-4)自动换行 |
|
||||
| 设计规范 | 大量引用 ui-ux-pro-max-skill/frontend-design | 精简为实际的视觉规范表格 + 设计辅助工具已使用说明 |
|
||||
| 版本历史 | 无 | 新增第 12 章版本历史 + 关联 CHANGELOG |
|
||||
|
||||
---
|
||||
|
||||
## 文件存档
|
||||
|
||||
| 文件 | 路径 |
|
||||
|------|------|
|
||||
| v0.9 原始 PRD | `docs/prd-history/OPC管理系统PRD-V0.9.md` |
|
||||
| v1.0 当前 PRD | `OPC管理系统PRD.md`(根目录) |
|
||||
| 修改日志(本文件) | `docs/prd/CHANGELOG.md` |
|
||||
918
docs/prd/OPC管理系统PRD-V0.9.md
Normal file
918
docs/prd/OPC管理系统PRD-V0.9.md
Normal file
@@ -0,0 +1,918 @@
|
||||
# OPC 管理系统 PRD
|
||||
|
||||
版本:v0.9
|
||||
日期:2026-05-29
|
||||
阶段:需求核对稿
|
||||
定位:以科普(慰心斋)为单项目独立版本,打造 OPC 工作台,从全局首页、销售、业务、运营、产品、财务 6 个视角,直观、清晰、全面地管理业务全貌。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 需求原文
|
||||
|
||||
以慰心斋为例,打造一个 OPC 工作台,让我能够直观地看到他销售、业务、运营、产品、财务的情况。另外,之前他为自己单独打造了一个财务体系,把这个财务 manager 系统直接合并进工作台里面,不再作为一个独立的系统。
|
||||
|
||||
### 1.2 背景
|
||||
|
||||
当前业务信息分散在销售表、方案文件、运营记录、产品路线图、财务台账等多个位置,难以一眼判断业务进展、交付风险、产品节奏和财务结果。
|
||||
|
||||
OPC 管理系统用于建立一个统一工作台,让负责人可以通过全局首页和 5 个业务 Tab 完成日常管理:
|
||||
|
||||
1. 全局首页:集中展示科普(慰心斋)的经营关键指标和风险提醒。
|
||||
2. 销售管理:看清目标客户、优先级、状态和多条时间线跟进记录。
|
||||
3. 业务方案:按版本管理售前/商务阶段的方案、成本、SOP、财务流程等文件。
|
||||
4. 运营管理:承接交付的实际管理形态,将项目分为业务机会项目和已签约执行项目,分别看清签约推进和 SOP 执行过程。
|
||||
5. 产品管理:按版本管理产品目标、核心功能、上线日期和版本跟进。
|
||||
6. 财务管理:用曲线图和明细表查看收入、毛利、成本/费用、净利。
|
||||
|
||||
本系统将承接并合并原有独立财务 manager 能力,财务不再作为单独系统存在,而是作为 OPC 管理系统中的一个 Tab。
|
||||
|
||||
### 1.3 样板范围
|
||||
|
||||
v0.9 以科普(慰心斋)作为首个单项目独立版本,优先接入和整理慰心斋已有资料:
|
||||
|
||||
- 销售资料:`/Users/mac/天机阁/地阁/慰心斋/1、销售管理`
|
||||
- 业务方案:`/Users/mac/天机阁/地阁/慰心斋/2、业务方案`
|
||||
- 运营资料:`/Users/mac/天机阁/地阁/慰心斋/3、运营方案`
|
||||
- 产品资料:`/Users/mac/天机阁/地阁/慰心斋/4、产品方案`
|
||||
- 财务系统:`/Users/mac/天机阁/地阁/慰心斋/5、财务管理/mananger`
|
||||
|
||||
首版只做单项目、单用户版本,不做多项目、多用户、权限体系。系统形态应保留后续复制到其他业务单元或升级为多项目/多用户的扩展能力,但首版数据、页面样例和迁移逻辑以科普(慰心斋)为准。
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户与使用场景
|
||||
|
||||
### 2.1 核心用户
|
||||
|
||||
- 业务负责人:查看全局进展,识别销售、运营、产品、财务风险。
|
||||
- 销售负责人:维护目标客户和跟进记录。
|
||||
- 项目/运营负责人:维护业务机会推进、已签约项目 SOP 执行、交付文件和项目进展。
|
||||
- 产品负责人:维护版本规划、上线日期和功能清单。
|
||||
- 财务负责人:维护收入、成本/费用、毛利、净利明细。
|
||||
|
||||
### 2.2 高频场景
|
||||
|
||||
- 每周例会前,先通过全局首页查看销售 pipeline、项目运营状态、产品版本进度、财务结果和风险提醒。
|
||||
- 新增客户后,录入客户优先级、当前状态和跟进记录。
|
||||
- 一个业务方案形成新版本后,上传方案、成本、SOP、财务流程文件。
|
||||
- 项目处于机会阶段时,在运营 Tab 中跟进签约推进;项目签约后,在运营 Tab 中按 SOP 跟进执行过程和交付文件。
|
||||
- 产品版本推进时,维护版本目标、核心功能、上线日期和跟进记录。
|
||||
- 财务复盘时,查看收入、毛利、成本/费用、净利月度趋势和明细。
|
||||
|
||||
---
|
||||
|
||||
## 3. 信息架构
|
||||
|
||||
系统顶部为全局标题和基础操作区,主体为 6 个一级 Tab:
|
||||
|
||||
1. 首页
|
||||
2. 销售管理
|
||||
3. 业务方案
|
||||
4. 运营
|
||||
5. 产品
|
||||
6. 财务
|
||||
|
||||
所有 Tab 均支持基础的新增、查看、编辑能力。删除能力作为后台管理能力保留,默认不突出展示,避免误删关键业务数据。
|
||||
|
||||
---
|
||||
|
||||
## 4. 功能需求
|
||||
|
||||
### 4.1 首页 Tab
|
||||
|
||||
首页是科普(慰心斋)单项目的经营总览页,用于在进入具体 Tab 前先看到整体情况。
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 首页由关键指标卡、风险提醒、近期动态和趋势概览组成。
|
||||
- 首页展示的是销售、业务、运营、产品、财务五个模块的聚合信息,不承载复杂编辑。
|
||||
|
||||
#### 关键指标
|
||||
|
||||
- P0 客户数
|
||||
- 跟进中销售机会数
|
||||
- 已签约执行项目数
|
||||
- 有风险执行项目数
|
||||
- 本月收入
|
||||
- 本月净利
|
||||
- 即将上线产品版本数
|
||||
|
||||
#### 风险提醒
|
||||
|
||||
- 预计签约时间临近但状态未推进的业务机会项目。
|
||||
- 已签约执行项目中标记为有风险或存在阻塞的项目。
|
||||
- 产品上线日期临近但状态未进入测试/已上线的版本。
|
||||
- 当月净利为负或成本/费用异常升高的月份。
|
||||
|
||||
#### 近期动态
|
||||
|
||||
- 最近新增的销售跟进记录。
|
||||
- 最近更新的运营项目跟进记录。
|
||||
- 最近上传或更新的业务方案/运营交付文件。
|
||||
- 最近新增的财务明细。
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 首页指标点击后可跳转到对应 Tab 并带入筛选条件。
|
||||
- 首页只做查看和跳转,不在首页直接编辑业务数据。
|
||||
|
||||
---
|
||||
|
||||
### 4.2 销售管理 Tab
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 内容按表格形式展示。
|
||||
- 表格字段:
|
||||
- 目标客户
|
||||
- 优先级
|
||||
- 状态
|
||||
- 最新跟进记录
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 用户可以新增销售线索/目标客户。
|
||||
- 用户点击表格中的某一行后,进入详情查看与编辑。
|
||||
- 详情中可以查看并编辑:
|
||||
- 目标客户
|
||||
- 优先级
|
||||
- 状态
|
||||
- 跟进记录必须是多条时间线记录,不使用单一文本字段。
|
||||
- 用户可以在详情中新增、编辑、删除每条跟进记录。
|
||||
|
||||
#### 跟进时间线字段
|
||||
|
||||
- 跟进时间
|
||||
- 跟进人
|
||||
- 跟进方式
|
||||
- 跟进内容
|
||||
- 下一步动作
|
||||
- 下次跟进时间
|
||||
|
||||
#### 推荐状态字段
|
||||
|
||||
- 待跟进
|
||||
- 跟进中
|
||||
- 方案中
|
||||
- 商务谈判
|
||||
- 已签约
|
||||
- 暂缓
|
||||
- 已丢单
|
||||
|
||||
#### 推荐优先级字段
|
||||
|
||||
- P0
|
||||
- P1
|
||||
- P2
|
||||
- P3
|
||||
|
||||
---
|
||||
|
||||
### 4.3 业务方案 Tab
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 一个版本一个卡片。
|
||||
- 卡片标题建议格式:客户/项目名称 + 版本号,例如「信达生物 v1.5」。
|
||||
|
||||
#### 每个版本卡片包含 4 个要素
|
||||
|
||||
1. 方案
|
||||
2. 成本
|
||||
3. SOP
|
||||
4. 财务流程
|
||||
|
||||
#### 文件规则
|
||||
|
||||
- 一个要素对应一个文件分组。
|
||||
- 每个文件分组允许上传多个文件。
|
||||
- 文件类型不限于:
|
||||
- Word
|
||||
- Excel
|
||||
- PPT
|
||||
- PDF
|
||||
- 图片
|
||||
- 压缩包
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 用户可以新增一个业务方案版本卡片。
|
||||
- 用户可以为卡片上传方案、成本、SOP、财务流程文件。
|
||||
- 用户可以查看每个要素下已上传的文件列表。
|
||||
- 用户可以替换、追加或删除文件。
|
||||
- 用户可以在线预览已上传文件。PDF 和图片应优先支持浏览器内预览;Word、Excel、PPT 可优先支持下载,若技术条件允许再支持在线预览或转换预览。
|
||||
- 用户可以编辑版本基础信息:
|
||||
- 客户/项目名称
|
||||
- 版本号
|
||||
- 版本说明
|
||||
- 创建日期
|
||||
- 当前状态
|
||||
|
||||
#### 推荐版本状态
|
||||
|
||||
- 草稿
|
||||
- 内部评审
|
||||
- 已提交客户
|
||||
- 客户反馈中
|
||||
- 已确认
|
||||
- 已归档
|
||||
|
||||
---
|
||||
|
||||
### 4.4 运营 Tab
|
||||
|
||||
运营是项目从机会推进到签约交付的实际承载形式。系统不单独设置“交付 Tab”,交付过程、交付文件、项目执行状态统一归入运营 Tab 管理。
|
||||
|
||||
运营项目分为两类:
|
||||
|
||||
1. 业务机会项目:尚未签约,以推动签约为核心目标,重点展示客户意向、签约推进状态、下一步动作和跟进记录。
|
||||
2. 已签约执行项目:已经签约,以按 SOP 交付为核心目标,重点展示 SOP 阶段、执行进度、交付物、风险和跟进记录。
|
||||
|
||||
首批运营文件来源:`/Users/mac/天机阁/地阁/慰心斋/3、运营方案`。当前已存在的文件按版本要求补充到对应运营项目,不建立“未归类文件池”。当前已存在的文件包括:
|
||||
|
||||
- 圆心科技--科普文章项目(1).pptx
|
||||
- 圆心科技-科普专访项目-2026年(1).pdf
|
||||
- 圆心科技-科普视频项目(1).pptx
|
||||
- 圆心科技《项目管理手册》-2026年.pdf
|
||||
- 科普项目-审核标准(文章-视频-音频).pdf
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 按表格形式展示。
|
||||
- 表格顶部提供项目类型筛选:
|
||||
- 业务机会项目
|
||||
- 已签约执行项目
|
||||
- 全部项目
|
||||
- 表格字段:
|
||||
- 项目名称
|
||||
- 项目类型
|
||||
- 项目状态
|
||||
- 当前阶段
|
||||
- 交付文件
|
||||
- 跟进记录
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 用户可以新增运营项目。
|
||||
- 新增项目时必须选择项目类型:业务机会项目或已签约执行项目。
|
||||
- 用户点击表格中的某一行后,以抽屉形式展开。
|
||||
- 抽屉内可以查看和编辑项目详情。
|
||||
- 抽屉内可以上传、在线预览、下载和管理项目相关运营/交付文件。
|
||||
- 运营文件必须先归属到明确项目版本,再根据该版本要求补充文件。
|
||||
- 已放入运营管理文件夹的文件,需要作为对应项目版本的初始文件导入或索引到运营 Tab。
|
||||
|
||||
#### 抽屉详情字段
|
||||
|
||||
- 项目名称
|
||||
- 项目版本
|
||||
- 项目类型
|
||||
- 项目状态
|
||||
- 当前阶段
|
||||
- 负责人
|
||||
- 起止时间
|
||||
- 交付文件列表
|
||||
- 跟进记录
|
||||
- 备注
|
||||
|
||||
#### 业务机会项目详情字段
|
||||
|
||||
- 目标客户
|
||||
- 业务机会名称
|
||||
- 客户需求
|
||||
- 预计签约金额
|
||||
- 预计签约时间
|
||||
- 签约概率
|
||||
- 当前推进阶段
|
||||
- 下一步动作
|
||||
- 跟进记录
|
||||
- 关联业务方案
|
||||
|
||||
#### 已签约执行项目详情字段
|
||||
|
||||
- 客户名称
|
||||
- 合同/项目名称
|
||||
- SOP 模板或 SOP 文件
|
||||
- 当前 SOP 阶段
|
||||
- 执行进度
|
||||
- 当前交付物
|
||||
- 风险与阻塞
|
||||
- 下一步动作
|
||||
- 跟进记录
|
||||
- 交付文件列表
|
||||
|
||||
#### 交付文件字段
|
||||
|
||||
- 文件名称
|
||||
- 文件类型
|
||||
- 所属项目
|
||||
- 所属项目版本
|
||||
- 文件分类
|
||||
- 文件路径
|
||||
- 上传时间
|
||||
- 备注
|
||||
|
||||
#### 推荐文件分类
|
||||
|
||||
- 项目方案
|
||||
- 项目管理手册
|
||||
- 审核标准
|
||||
- 执行 SOP
|
||||
- 交付物
|
||||
- 复盘报告
|
||||
|
||||
#### 推荐项目类型
|
||||
|
||||
- 业务机会项目
|
||||
- 已签约执行项目
|
||||
|
||||
#### 业务机会项目推荐状态
|
||||
|
||||
- 线索发现
|
||||
- 需求沟通
|
||||
- 方案准备
|
||||
- 方案已提交
|
||||
- 商务谈判
|
||||
- 待签约
|
||||
- 已签约
|
||||
- 暂缓
|
||||
- 已丢单
|
||||
|
||||
#### 已签约执行项目推荐状态
|
||||
|
||||
- 未启动
|
||||
- SOP 执行中
|
||||
- 有风险
|
||||
- 待客户确认
|
||||
- 已完成
|
||||
- 已归档
|
||||
|
||||
#### SOP 阶段建议
|
||||
|
||||
- 项目启动
|
||||
- 需求确认
|
||||
- 方案确认
|
||||
- 内容生产
|
||||
- 审核确认
|
||||
- 渠道分发
|
||||
- 数据/效果回收
|
||||
- 复盘验收
|
||||
|
||||
---
|
||||
|
||||
### 4.5 产品 Tab
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 一个版本一个卡片。
|
||||
- 卡片字段:
|
||||
- 版本目标
|
||||
- 核心功能清单
|
||||
- 上线日期
|
||||
- 跟进记录
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 用户可以新增产品版本。
|
||||
- 用户点击版本卡片后,以抽屉形式展开查看版本详情。
|
||||
- 用户可以在抽屉中修改对应内容。
|
||||
|
||||
#### 抽屉详情字段
|
||||
|
||||
- 产品名称
|
||||
- 版本号
|
||||
- 版本目标
|
||||
- 核心功能清单
|
||||
- 上线日期
|
||||
- 当前状态
|
||||
- 跟进记录
|
||||
- 备注
|
||||
|
||||
#### 推荐版本状态
|
||||
|
||||
- 规划中
|
||||
- 设计中
|
||||
- 开发中
|
||||
- 测试中
|
||||
- 已上线
|
||||
- 已延期
|
||||
- 已取消
|
||||
|
||||
---
|
||||
|
||||
### 4.6 财务 Tab
|
||||
|
||||
#### 页面结构
|
||||
|
||||
财务 Tab 分为趋势图区域和明细区域。
|
||||
|
||||
#### 趋势图
|
||||
|
||||
需要展示 4 条月度曲线:
|
||||
|
||||
1. 收入按月份的曲线图
|
||||
2. 毛利按月份的曲线图
|
||||
3. 成本/费用按月份的曲线图
|
||||
4. 净利按月份的曲线图
|
||||
|
||||
#### 明细表
|
||||
|
||||
每个指标需要对应明细:
|
||||
|
||||
1. 收入明细
|
||||
2. 毛利明细
|
||||
3. 成本/费用明细
|
||||
4. 净利明细
|
||||
|
||||
#### 推荐财务口径
|
||||
|
||||
- 收入:当月确认收入金额。
|
||||
- 成本/费用:统一录入为成本/费用,再通过类型区分直接成本、期间费用、运营费用、管理费用等。
|
||||
- 毛利:收入 - 成本。
|
||||
- 净利:收入 - 成本/费用。
|
||||
- 月份统计按自然月,月份格式为 YYYY-MM。
|
||||
|
||||
#### 明细字段
|
||||
|
||||
收入明细:
|
||||
|
||||
- 月份
|
||||
- 客户/项目
|
||||
- 收入类型
|
||||
- 金额
|
||||
- 确认日期
|
||||
- 备注
|
||||
|
||||
成本/费用明细:
|
||||
|
||||
- 月份
|
||||
- 客户/项目
|
||||
- 类型
|
||||
- 金额
|
||||
- 发生日期
|
||||
- 备注
|
||||
|
||||
净利明细:
|
||||
|
||||
- 月份
|
||||
- 收入合计
|
||||
- 成本/费用合计
|
||||
- 毛利
|
||||
- 净利
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据对象
|
||||
|
||||
### 5.1 SalesLead 销售线索
|
||||
|
||||
- id
|
||||
- target_customer
|
||||
- priority
|
||||
- status
|
||||
- latest_follow_up_record
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
### 5.2 FollowUpRecord 跟进时间线记录
|
||||
|
||||
- id
|
||||
- target_type
|
||||
- target_id
|
||||
- followed_at
|
||||
- follower
|
||||
- follow_up_method
|
||||
- content
|
||||
- next_action
|
||||
- next_follow_up_at
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
target_type 包括:
|
||||
|
||||
- sales:销售线索
|
||||
- operation:运营项目
|
||||
- product:产品版本
|
||||
|
||||
### 5.3 BusinessProposal 业务方案版本
|
||||
|
||||
- id
|
||||
- customer_or_project_name
|
||||
- version
|
||||
- description
|
||||
- status
|
||||
- created_date
|
||||
- files
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
files 按要素分组:
|
||||
|
||||
- proposal_files
|
||||
- cost_files
|
||||
- sop_files
|
||||
- finance_process_files
|
||||
|
||||
### 5.4 OperationProject 运营项目
|
||||
|
||||
- id
|
||||
- project_name
|
||||
- project_version
|
||||
- project_type
|
||||
- project_status
|
||||
- current_stage
|
||||
- owner
|
||||
- start_date
|
||||
- end_date
|
||||
- target_customer
|
||||
- customer_need
|
||||
- expected_contract_amount
|
||||
- expected_sign_date
|
||||
- sign_probability
|
||||
- next_action
|
||||
- related_business_proposal_id
|
||||
- sop_file_id
|
||||
- sop_stage
|
||||
- execution_progress
|
||||
- current_deliverable
|
||||
- risks
|
||||
- delivery_files
|
||||
- follow_up_record
|
||||
- notes
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
project_type 包括:
|
||||
|
||||
- opportunity:业务机会项目
|
||||
- execution:已签约执行项目
|
||||
|
||||
说明:
|
||||
|
||||
- 当 project_type = opportunity 时,重点使用目标客户、客户需求、预计签约金额、预计签约时间、签约概率、下一步动作、关联业务方案等字段。
|
||||
- 当 project_type = execution 时,重点使用 SOP 文件、SOP 阶段、执行进度、当前交付物、风险、交付文件等字段。
|
||||
|
||||
### 5.5 OperationFile 运营/交付文件
|
||||
|
||||
- id
|
||||
- project_id
|
||||
- project_version
|
||||
- file_name
|
||||
- file_type
|
||||
- file_category
|
||||
- file_path
|
||||
- uploaded_at
|
||||
- notes
|
||||
|
||||
### 5.6 ProductVersion 产品版本
|
||||
|
||||
- id
|
||||
- product_name
|
||||
- version
|
||||
- version_goal
|
||||
- feature_list
|
||||
- launch_date
|
||||
- status
|
||||
- follow_up_record
|
||||
- notes
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
### 5.7 FinanceRecord 财务记录
|
||||
|
||||
- id
|
||||
- month
|
||||
- project_name
|
||||
- record_type
|
||||
- category
|
||||
- amount
|
||||
- occurred_date
|
||||
- notes
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
record_type 包括:
|
||||
|
||||
- revenue
|
||||
- cost_expense
|
||||
|
||||
month 采用自然月格式 YYYY-MM。
|
||||
|
||||
成本和费用统一进入 cost_expense,再通过 category 区分类型。毛利和净利由系统根据收入、成本/费用自动计算。
|
||||
|
||||
### 5.8 AppScope 系统范围
|
||||
|
||||
- project_name:科普(慰心斋)
|
||||
- user_mode:single_user
|
||||
- project_mode:single_project
|
||||
- multi_user_enabled:false
|
||||
- multi_project_enabled:false
|
||||
|
||||
---
|
||||
|
||||
## 6. 页面与交互规范
|
||||
|
||||
### 6.1 通用能力
|
||||
|
||||
- 所有列表支持新增。
|
||||
- 所有详情支持编辑和保存。
|
||||
- 所有日期字段使用日期选择器。
|
||||
- 所有状态字段使用下拉选择。
|
||||
- 跟进记录统一使用多条时间线记录。
|
||||
- 保存后列表与详情数据同步更新。
|
||||
|
||||
### 6.2 抽屉交互
|
||||
|
||||
运营 Tab 和产品 Tab 使用右侧抽屉。
|
||||
|
||||
- 点击表格行或版本卡片打开抽屉。
|
||||
- 抽屉展示完整详情。
|
||||
- 抽屉内提供编辑和保存。
|
||||
- 关闭抽屉时,如存在未保存修改,需要提示用户确认。
|
||||
|
||||
### 6.3 文件上传
|
||||
|
||||
业务方案 Tab 和运营 Tab 支持文件上传与在线预览。
|
||||
|
||||
- 上传文件必须归属于某个方案版本和某个要素。
|
||||
- 上传后展示文件名、大小、上传时间。
|
||||
- 文件必须支持下载。
|
||||
- 文件必须支持在线预览。PDF 和图片优先使用浏览器内预览;Word、Excel、PPT 可以先提供基础预览能力或转换预览方案,若首版无法直接预览,需要在界面中明确提示并保留下载入口。
|
||||
- 运营文件必须先归属到明确项目版本,再按该版本要求补充对应文件。
|
||||
|
||||
---
|
||||
|
||||
## 7. 技术栈与实现约束
|
||||
|
||||
### 7.1 前端技术栈
|
||||
|
||||
OPC 工作台前端采用:
|
||||
|
||||
- Jinja2:负责服务端页面模板渲染。
|
||||
- Tailwind CSS CDN:负责样式系统、布局、间距、颜色和响应式规则,不使用 Vite 构建链。
|
||||
- 原生 JavaScript:负责 Tab 切换、抽屉详情、表单提交、文件上传、数据刷新。
|
||||
- Lucide 浏览器版:作为图标库,负责导航、操作按钮、文件预览/下载等线性图标。
|
||||
- Chart.js CDN:负责财务趋势图,包括收入、毛利、成本/费用、净利的月度曲线。
|
||||
|
||||
### 7.2 后端技术栈
|
||||
|
||||
OPC 工作台后端采用:
|
||||
|
||||
- Flask:负责页面渲染、REST API、文件上传、文件访问、在线预览入口和静态资源服务。
|
||||
- sqlite3:负责 SQLite 数据读写,首版不引入 ORM。
|
||||
- Jinja2:随 Flask 用于模板渲染。
|
||||
- Flask development server:负责本地单用户运行。
|
||||
|
||||
### 7.3 数据库与存储
|
||||
|
||||
首版数据库采用 SQLite。
|
||||
|
||||
- 主数据库文件建议为 `data/opc.sqlite`。
|
||||
- 原财务 manager 的 SQLite 数据需要迁移或合并到 OPC 主数据库,不再作为独立系统运行。
|
||||
- SQLite 中存储销售、跟进时间线、业务方案版本、运营项目、运营/交付文件元数据、产品版本、财务记录等结构化数据。
|
||||
- 上传文件本体存储在本地文件系统,不直接写入 SQLite。
|
||||
- 上传文件目录建议为 `data/uploads/`。
|
||||
- SQLite 仅保存文件元数据,包括文件名、类型、大小、所属模块、所属项目/版本、文件路径、上传时间。
|
||||
- 后端需要提供文件下载和在线预览访问 URL。
|
||||
|
||||
### 7.4 财务 manager 合并策略
|
||||
|
||||
原财务 manager 系统位于 `/Users/mac/天机阁/地阁/慰心斋/5、财务管理/mananger`。
|
||||
|
||||
合并要求:
|
||||
|
||||
- 不保留原财务 manager 作为独立入口。
|
||||
- 复用或迁移原财务数据结构中的客户、回款、成本、文件索引等有价值数据。
|
||||
- 将原财务 manager 的财务能力合并到 OPC 的财务 Tab。
|
||||
- 财务数据最终统一进入 OPC 主数据库。
|
||||
- 如存在字段差异,以 OPC v0.9 的 FinanceRecord 和财务口径为准。
|
||||
|
||||
### 7.5 API 设计原则
|
||||
|
||||
- API 采用 REST 风格。
|
||||
- 所有业务对象提供基础 CRUD 接口。
|
||||
- 文件上传接口必须接收模块、项目/版本、文件分类等归属信息。
|
||||
- 财务汇总接口由后端根据明细计算收入、毛利、成本/费用、净利月度数据。
|
||||
- 首页指标接口由后端聚合销售、运营、产品、财务数据生成。
|
||||
- 首版不做登录、鉴权、多用户权限和租户隔离。
|
||||
|
||||
### 7.6 技术栈使用原则
|
||||
|
||||
- 不使用 Ant Design 作为首版组件库。
|
||||
- 不新增重型前端框架或多套 UI 组件体系,避免视觉和交互风格分裂。
|
||||
- Tailwind CSS CDN 需要承接设计系统中的颜色、间距、字号、圆角、阴影和状态表达。
|
||||
- 不使用 React、Vite、Rollup、shadcn/ui 或 Recharts 作为首版运行依赖。
|
||||
- Chart.js 图表需要与财务明细数据同源,避免图表和表格口径不一致。
|
||||
- 后端沿用 Flask 技术路线,但不保留原财务 manager 独立系统;统一实现为 OPC Flask 应用。
|
||||
- 数据库首版不引入 PostgreSQL、MySQL 等独立数据库服务。
|
||||
|
||||
---
|
||||
|
||||
## 8. 设计质量与 UI 要求
|
||||
|
||||
### 8.1 设计目标
|
||||
|
||||
OPC 工作台应采用企业级经营管理后台风格,重点是清晰、稳定、可扫描、适合会议复盘。不做营销页风格,不使用过度装饰、强动效或大面积渐变。
|
||||
|
||||
### 8.2 推荐视觉方向
|
||||
|
||||
- 主色建议采用稳重蓝色系,辅以红/橙/绿表达风险、待处理、完成等状态。
|
||||
- 布局以白底、浅灰分区、细边框、紧凑表格为主。
|
||||
- 首屏应直接进入工作台,不设置营销型 landing page。
|
||||
- 销售、运营、财务等高频经营信息优先保证信息密度和可读性。
|
||||
- 卡片只用于业务方案版本和产品版本,不把所有区块都做成大卡片。
|
||||
- 抽屉用于承载详情编辑,避免跳转打断上下文。
|
||||
|
||||
### 8.3 组件风格要求
|
||||
|
||||
- 销售管理:表格 + 状态标签 + 详情编辑。
|
||||
- 业务方案:版本卡片 + 文件分组上传。
|
||||
- 运营:项目表格 + 项目类型筛选 + 抽屉详情。
|
||||
- 产品:版本卡片 + 抽屉详情。
|
||||
- 财务:月度趋势折线图 + 明细表。
|
||||
|
||||
### 8.4 设计辅助工具分工
|
||||
|
||||
首版开发应同时使用 `ui-ux-pro-max-skill` 和 `frontend-design`,二者分工如下:
|
||||
|
||||
- `ui-ux-pro-max-skill`:负责设计系统生成、dashboard 风格参考、UI 审计框架。
|
||||
- `frontend-design`:负责前端页面落地质量,包括布局、信息层级、组件组合、表格密度、抽屉体验、状态标签、图表与明细联动、响应式细节。
|
||||
- Flask + Jinja2 + Tailwind CSS CDN + Lucide + Chart.js + 原生 JavaScript:负责实际工程实现。
|
||||
|
||||
### 8.5 ui-ux-pro-max-skill 使用要求
|
||||
|
||||
开发前应使用 `ui-ux-pro-max-skill` 辅助生成或校准设计系统,用于提升 UI/UX 质量。
|
||||
|
||||
使用目标:
|
||||
|
||||
- 生成 OPC 工作台的设计系统建议,包括色彩、字体、间距、表格密度、状态标签和图表风格。
|
||||
- 针对 sales dashboard、operations dashboard、financial dashboard、executive dashboard 等场景获取布局参考。
|
||||
- 对最终页面进行 UI 审计,检查信息层级、对比度、间距一致性、状态表达、图表可读性和表格扫描效率。
|
||||
|
||||
使用边界:
|
||||
|
||||
- `ui-ux-pro-max-skill` 作为设计系统和 UI 审计辅助,不作为组件库。
|
||||
- 实际开发必须使用 Flask + Jinja2 + Tailwind CSS CDN + Lucide + Chart.js + 原生 JavaScript + SQLite 落地。
|
||||
- 不允许为了追求视觉效果牺牲经营信息的清晰度。
|
||||
|
||||
建议查询方向:
|
||||
|
||||
- enterprise operations dashboard
|
||||
- sales pipeline dashboard
|
||||
- project operations dashboard
|
||||
- financial dashboard
|
||||
- executive dashboard
|
||||
|
||||
交付要求:
|
||||
|
||||
- 开发前输出一份 OPC 工作台设计规范摘要。
|
||||
- 首版开发完成后,基于 `ui-ux-pro-max-skill` 的审计思路输出一份 UI 自检结果。
|
||||
|
||||
### 8.6 frontend-design 使用要求
|
||||
|
||||
开发前和开发过程中应使用 `frontend-design` 辅助提高页面实现质量。
|
||||
|
||||
使用目标:
|
||||
|
||||
- 将 `ui-ux-pro-max-skill` 产出的设计系统要求落到具体页面结构和组件组合。
|
||||
- 校准首页、销售表格、业务方案卡片、运营抽屉、产品版本卡片、财务图表的布局和信息层级。
|
||||
- 检查表格是否紧凑可扫描,抽屉是否适合编辑详情,卡片是否避免过度嵌套,图表和明细是否容易核对。
|
||||
- 检查 Tailwind class、Lucide 图标、Chart.js 图表和原生交互是否保持统一视觉语言。
|
||||
|
||||
使用边界:
|
||||
|
||||
- `frontend-design` 作为页面设计和前端实现质量辅助,不替代 PRD,不改变已确认的业务逻辑。
|
||||
- 不为了视觉效果增加不必要的动画、营销页结构或复杂交互。
|
||||
- 若 `frontend-design` 建议与业务可读性冲突,以业务可读性和会议复盘效率优先。
|
||||
|
||||
交付要求:
|
||||
|
||||
- 开发前输出一份页面级 UI 落地清单。
|
||||
- 首版开发完成后,基于 `frontend-design` 的检查思路完成页面级自检。
|
||||
|
||||
---
|
||||
|
||||
## 9. 首页指标要求
|
||||
|
||||
v0.9 必须加入全局首页概览,建议展示:
|
||||
|
||||
- P0 客户数
|
||||
- 已签约执行项目数
|
||||
- 有风险项目数
|
||||
- 本月收入
|
||||
- 本月净利
|
||||
- 即将上线产品版本数
|
||||
|
||||
首页作为默认进入页,点击指标可跳转到对应 Tab。
|
||||
|
||||
---
|
||||
|
||||
## 10. 非功能需求
|
||||
|
||||
- 数据应持久化保存,刷新页面不丢失。
|
||||
- 财务数据计算结果应与明细一致。
|
||||
- 文件上传后应可追溯到对应业务方案版本。
|
||||
- 运营文件上传后应可追溯到对应项目版本。
|
||||
- 页面应支持桌面端优先使用。
|
||||
- 表格内容应清晰紧凑,适合会议和经营复盘场景。
|
||||
- 系统应保留后续复用到其他业务单元的扩展能力。
|
||||
- 首版开发前应完成设计规范摘要,开发后应完成 UI 自检。
|
||||
- 首版开发前应完成页面级 UI 落地清单,开发后应完成页面级自检。
|
||||
- 前端实现必须符合 Jinja2 + Tailwind CSS CDN + Lucide + Chart.js + 原生 JavaScript 技术栈约束。
|
||||
- 后端实现必须符合 Flask + sqlite3 + SQLite 技术栈约束。
|
||||
- 上传文件必须采用本地文件系统存储,SQLite 存储文件元数据。
|
||||
- 首版为单用户、单项目版本,项目名称为科普(慰心斋)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 验收标准
|
||||
|
||||
### 首页
|
||||
|
||||
- 默认进入系统后展示全局首页。
|
||||
- 首页展示 P0 客户数、跟进中销售机会数、已签约执行项目数、有风险执行项目数、本月收入、本月净利、即将上线产品版本数。
|
||||
- 首页展示风险提醒和近期动态。
|
||||
- 点击首页指标可以跳转到对应 Tab。
|
||||
|
||||
### 销售管理
|
||||
|
||||
- 可以新增销售客户。
|
||||
- 可以在表格中看到目标客户、优先级、状态、最新跟进记录。
|
||||
- 点击行后可以查看和编辑详情。
|
||||
- 销售跟进记录以多条时间线形式展示和维护。
|
||||
|
||||
### 业务方案
|
||||
|
||||
- 可以新增方案版本卡片。
|
||||
- 每个卡片包含方案、成本、SOP、财务流程 4 个文件要素。
|
||||
- 每个要素可以上传多个文件。
|
||||
- 上传后文件能够在线预览和下载。
|
||||
|
||||
### 运营
|
||||
|
||||
- 可以新增运营项目。
|
||||
- 可以将运营项目分为业务机会项目和已签约执行项目。
|
||||
- 可以在表格中看到项目名称、项目类型、项目状态、当前阶段、交付文件、跟进记录。
|
||||
- 点击项目后可以用抽屉查看和编辑详情。
|
||||
- 业务机会项目的抽屉重点展示和编辑签约推进信息,包括客户需求、预计签约金额、预计签约时间、签约概率、下一步动作和跟进记录。
|
||||
- 已签约执行项目的抽屉重点展示和编辑 SOP 执行信息,包括 SOP 文件、当前 SOP 阶段、执行进度、当前交付物、风险和跟进记录。
|
||||
- 可以在抽屉中上传、在线预览、下载、删除项目相关运营/交付文件。
|
||||
- 运营文件按项目版本补充,不建立未归类文件池。
|
||||
- 可以按项目版本索引或导入 `/Users/mac/天机阁/地阁/慰心斋/3、运营方案` 中已有文件。
|
||||
|
||||
### 产品
|
||||
|
||||
- 可以新增产品版本卡片。
|
||||
- 卡片展示版本目标、核心功能清单、上线日期、跟进记录。
|
||||
- 点击卡片后可以用抽屉查看和编辑版本详情。
|
||||
|
||||
### 财务
|
||||
|
||||
- 可以录入收入、成本/费用明细。
|
||||
- 系统按月份展示收入、毛利、成本/费用、净利曲线。
|
||||
- 系统可以展示对应明细。
|
||||
- 月份按自然月统计,格式为 YYYY-MM。
|
||||
- 成本和费用统一作为成本/费用录入,再通过类型区分。
|
||||
- 净利 = 收入 - 成本/费用。
|
||||
|
||||
### UI/UX
|
||||
|
||||
- 已使用 `ui-ux-pro-max-skill` 生成或校准 OPC 工作台设计规范摘要。
|
||||
- 已使用 `frontend-design` 生成页面级 UI 落地清单。
|
||||
- 前端页面基于 Jinja2 + Tailwind CSS CDN + 原生 JavaScript 实现。
|
||||
- 图标库基于 Lucide 浏览器版实现。
|
||||
- 财务趋势图基于 Chart.js 实现。
|
||||
- 页面符合企业级经营管理后台风格,信息清晰、表格紧凑、状态可扫描。
|
||||
- 销售、运营、产品详情通过抽屉承载,避免频繁跳转。
|
||||
- 财务图表与明细表可对应核对。
|
||||
- 首版完成后已基于 `ui-ux-pro-max-skill` 和 `frontend-design` 进行 UI 自检,并记录主要问题和修正结果。
|
||||
|
||||
### 后端与数据
|
||||
|
||||
- 后端基于 Flask 实现页面渲染、REST API 和文件服务。
|
||||
- 数据读写基于 sqlite3 实现。
|
||||
- 数据持久化使用 SQLite,主数据库为 OPC 主库。
|
||||
- 原财务 manager 不再作为独立系统运行,其能力合并进 OPC 财务 Tab。
|
||||
- 文件本体存储在本地 uploads 目录,SQLite 存储文件元数据。
|
||||
- 后端提供文件上传、下载和在线预览访问入口。
|
||||
- 首页指标和财务月度曲线由后端基于明细数据计算生成。
|
||||
|
||||
---
|
||||
|
||||
## 12. 已确认决策
|
||||
|
||||
1. v0.9 必须加入全局首页概览。
|
||||
2. 销售管理中的跟进记录使用多条时间线记录。
|
||||
3. 业务方案和运营交付文件需要支持在线预览和下载。
|
||||
4. 运营文件不是先进入文件池再绑定项目,而是先确定项目版本,再按版本要求补充文件。
|
||||
5. 财务图表按自然月统计,月份格式为 YYYY-MM。
|
||||
6. 财务明细中的成本和费用统一为成本/费用,再按类型分类。
|
||||
7. 首版先做单用户、单项目版本。
|
||||
8. 单项目名称为科普(慰心斋),作为独立版本落地。
|
||||
9. 后续再考虑多项目、多用户、权限体系。
|
||||
10. 开发前同时使用 `ui-ux-pro-max-skill` 和 `frontend-design` 提升 UI/UX 质量。
|
||||
11. 后端采用 Flask + sqlite3,数据库采用 SQLite。
|
||||
12. 文件本体存本地文件系统,数据库只存文件元数据。
|
||||
13. 原财务 manager 合并进 OPC,不再独立运行。
|
||||
14. 前端采用 Jinja2 + Tailwind CSS CDN + Lucide + Chart.js + 原生 JavaScript,不使用 Vite 构建链。
|
||||
844
docs/prd/OPC管理系统PRD-V1.0.md
Normal file
844
docs/prd/OPC管理系统PRD-V1.0.md
Normal file
@@ -0,0 +1,844 @@
|
||||
# OPC 管理系统 PRD
|
||||
|
||||
版本:v1.0
|
||||
日期:2026-06-01
|
||||
阶段:实际落地版(匹配 OPC-Manager v1.0.2 代码实现)
|
||||
定位:以科普(慰心斋)为单项目独立版本,打造 OPC 工作台,从全局首页、销售、业务、运营、产品、财务 6 个视角,直观、清晰、全面地管理业务全貌。
|
||||
|
||||
> 历史版本参见 `OPC管理系统PRD-V0.9.md`,修改日志见 `CHANGELOG.md`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 需求原文
|
||||
|
||||
以慰心斋为例,打造一个 OPC 工作台,让我能够直观地看到他销售、业务、运营、产品、财务的情况。另外,之前他为自己单独打造了一个财务体系,把这个财务 manager 系统直接合并进工作台里面,不再作为一个独立的系统。
|
||||
|
||||
### 1.2 背景
|
||||
|
||||
当前业务信息分散在销售表、方案文件、运营记录、产品路线图、财务台账等多个位置,难以一眼判断业务进展、交付风险、产品节奏和财务结果。
|
||||
|
||||
OPC 管理系统用于建立一个统一工作台,让负责人可以通过全局首页和 5 个业务 Tab 完成日常管理:
|
||||
|
||||
1. 全局首页:集中展示科普(慰心斋)的经营关键指标和风险提醒。
|
||||
2. 销售管理:看清目标客户、优先级、状态和多条时间线跟进记录。
|
||||
3. 业务方案:按版本管理售前/商务阶段的方案、成本、SOP、财务流程等文件。
|
||||
4. 运营管理:承接交付的实际管理形态,将项目分为业务机会项目和已签约执行项目,分别看清签约推进和 SOP 执行过程。
|
||||
5. 产品管理:按版本管理产品目标、核心功能、上线日期和版本跟进。
|
||||
6. 财务管理:用曲线图和明细表查看收入、毛利、成本/费用、净利。
|
||||
|
||||
本系统将承接并合并原有独立财务 manager 能力,财务不再作为单独系统存在,而是作为 OPC 管理系统中的一个 Tab。
|
||||
|
||||
### 1.3 样板范围
|
||||
|
||||
v1.0 以科普(慰心斋)作为首个单项目独立版本,优先接入和整理慰心斋已有资料:
|
||||
|
||||
- 销售资料:`/Users/mac/天机阁/地阁/慰心斋/1、销售管理`
|
||||
- 业务方案:`/Users/mac/天机阁/地阁/慰心斋/2、业务方案`
|
||||
- 运营资料:`/Users/mac/天机阁/地阁/慰心斋/3、运营方案`
|
||||
- 产品资料:`/Users/mac/天机阁/地阁/慰心斋/4、产品方案`
|
||||
- 财务系统:`/Users/mac/天机阁/地阁/慰心斋/5、财务管理/mananger`
|
||||
|
||||
首版只做单项目、单用户版本,不做多项目、多用户、权限体系。系统形态应保留后续复制到其他业务单元或升级为多项目/多用户的扩展能力,但首版数据、页面样例和迁移逻辑以科普(慰心斋)为准。
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户与使用场景
|
||||
|
||||
### 2.1 核心用户
|
||||
|
||||
- 业务负责人:查看全局进展,识别销售、运营、产品、财务风险。
|
||||
- 销售负责人:维护目标客户和跟进记录。
|
||||
- 项目/运营负责人:维护业务机会推进、已签约项目 SOP 执行、交付文件和项目进展。
|
||||
- 产品负责人:维护版本规划、上线日期和功能清单。
|
||||
- 财务负责人:维护收入、成本/费用、毛利、净利明细。
|
||||
|
||||
### 2.2 高频场景
|
||||
|
||||
- 每周例会前,先通过全局首页查看销售 pipeline、项目运营状态、产品版本进度、财务结果和风险提醒。
|
||||
- 新增客户后,录入客户优先级、当前状态和跟进记录。
|
||||
- 一个业务方案形成新版本后,上传方案、成本、SOP、财务流程文件。
|
||||
- 项目处于机会阶段时,在运营 Tab 中跟进签约推进;项目签约后,在运营 Tab 中按 SOP 跟进执行过程和交付文件。
|
||||
- 产品版本推进时,维护版本目标、核心功能、上线日期和跟进记录。
|
||||
- 财务复盘时,查看收入、毛利、成本/费用、净利月度趋势和明细。
|
||||
|
||||
---
|
||||
|
||||
## 3. 信息架构
|
||||
|
||||
系统顶部为全局标题和基础操作区,主体为 6 个一级 Tab:
|
||||
|
||||
1. 首页
|
||||
2. 销售管理
|
||||
3. 业务方案
|
||||
4. 运营
|
||||
5. 产品
|
||||
6. 财务
|
||||
|
||||
所有 Tab 均支持基础的新增、查看、编辑能力。删除能力作为后台管理能力保留,默认不突出展示,避免误删关键业务数据。
|
||||
|
||||
---
|
||||
|
||||
## 4. 功能需求
|
||||
|
||||
### 4.1 首页 Tab
|
||||
|
||||
首页是科普(慰心斋)单项目的经营总览页,用于在进入具体 Tab 前先看到整体情况。
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 首页由关键指标卡、财务趋势图、风险提醒和近期动态组成。
|
||||
- 首页展示的是销售、业务、运营、产品、财务五个模块的聚合信息,不承载复杂编辑。
|
||||
|
||||
#### 关键指标(7 项,4 列网格自动换行)
|
||||
|
||||
- P0 客户数
|
||||
- 跟进中销售机会数
|
||||
- 已签约执行项目数
|
||||
- 有风险项目数
|
||||
- 本月收入
|
||||
- 本月净利
|
||||
- 即将上线产品版本数
|
||||
|
||||
#### 风险提醒
|
||||
|
||||
- 预计签约时间临近但状态未推进的业务机会项目。
|
||||
- 已签约执行项目中标记为有风险或存在阻塞的项目。
|
||||
- 产品上线日期临近但状态未进入测试/已上线的版本。
|
||||
- 当月净利为负或成本/费用异常升高的月份。
|
||||
|
||||
#### 近期动态
|
||||
|
||||
- 最近新增的销售跟进记录。
|
||||
- 最近更新的运营项目跟进记录。
|
||||
- 最近上传或更新的业务方案/运营交付文件。
|
||||
- 最近新增的财务明细。
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 首页指标点击后可跳转到对应 Tab 并带入筛选条件。
|
||||
- 首页只做查看和跳转,不在首页直接编辑业务数据。
|
||||
- 财务趋势图容器固定高度 140px,配合 Chart.js `maintainAspectRatio: false`。
|
||||
|
||||
---
|
||||
|
||||
### 4.2 销售管理 Tab
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 内容按表格形式展示。
|
||||
- 表格字段:
|
||||
- 目标客户
|
||||
- 优先级
|
||||
- 状态
|
||||
- 最新跟进记录
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 用户可以新增销售线索/目标客户。
|
||||
- 用户点击表格中的某一行后,以抽屉形式打开详情(720px 宽)。
|
||||
- 抽屉内详情字段:
|
||||
- 目标客户
|
||||
- 优先级(下拉选择)
|
||||
- 状态(下拉选择)
|
||||
- 字段失焦自动保存,不需要保存按钮。
|
||||
- 抽屉右上角关闭按钮关闭。
|
||||
- 跟进记录必须是多条时间线记录,不使用单一文本字段。
|
||||
- 用户可以在详情中通过 Squire 富文本编辑器新增评论,支持删除已有评论。
|
||||
|
||||
#### 跟进时间线字段
|
||||
|
||||
- 跟进时间
|
||||
- 跟进人
|
||||
- 跟进方式
|
||||
- 跟进内容
|
||||
- 下一步动作
|
||||
- 下次跟进时间
|
||||
|
||||
#### 推荐状态字段
|
||||
|
||||
- 待跟进
|
||||
- 跟进中
|
||||
- 方案中
|
||||
- 商务谈判
|
||||
- 已签约
|
||||
- 暂缓
|
||||
- 已丢单
|
||||
|
||||
#### 推荐优先级字段
|
||||
|
||||
- P0
|
||||
- P1
|
||||
- P2
|
||||
- P3
|
||||
|
||||
---
|
||||
|
||||
### 4.3 业务方案 Tab
|
||||
|
||||
> **v1.0 变更**:从 v0.9 的"一个版本一个卡片"改为表格布局,保持与销售、运营一致的交互模式。
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 按表格形式展示。
|
||||
- 表格字段:
|
||||
- 客户/项目名称
|
||||
- 版本号
|
||||
- 状态
|
||||
- 文件数
|
||||
|
||||
#### 每个版本包含 4 个文件要素
|
||||
|
||||
1. 方案
|
||||
2. 成本
|
||||
3. SOP
|
||||
4. 财务流程
|
||||
|
||||
#### 文件规则
|
||||
|
||||
- 一个要素对应一个文件分组。
|
||||
- 每个文件分组允许上传多个文件。
|
||||
- 文件类型不限于:Word、Excel、PPT、PDF、图片、压缩包。
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 用户可以新增一个业务方案版本。
|
||||
- 用户点击表格行后,以抽屉形式展开(720px 宽)。
|
||||
- 抽屉内展示两大部分:
|
||||
1. **属性区**:客户/项目名称、版本号、版本说明、创建日期、状态(均可编辑,失焦自动保存)
|
||||
2. **方案文件区**:按方案/成本/SOP/财务流程 4 个分组展示,每组支持上传、预览、下载、删除文件
|
||||
- 用户可以查看每个要素下已上传的文件列表。
|
||||
- 用户可以替换、追加或删除文件。
|
||||
- 用户可以在线预览已上传文件。PDF 和图片优先支持浏览器内预览;Word、Excel、PPT 提供下载。
|
||||
- 用户可编辑版本基础信息:客户/项目名称、版本号、版本说明、创建日期、当前状态。
|
||||
- 抽屉内支持 Squire 富文本评论,评论支持删除。
|
||||
|
||||
#### 推荐版本状态
|
||||
|
||||
- 草稿
|
||||
- 内部评审
|
||||
- 已提交客户
|
||||
- 客户反馈中
|
||||
- 已确认
|
||||
- 已归档
|
||||
|
||||
---
|
||||
|
||||
### 4.4 运营 Tab
|
||||
|
||||
运营是项目从机会推进到签约交付的实际承载形式。系统不单独设置"交付 Tab",交付过程、交付文件、项目执行状态统一归入运营 Tab 管理。
|
||||
|
||||
运营项目分为两类:
|
||||
|
||||
1. 业务机会项目:尚未签约,以推动签约为核心目标,重点展示客户意向、签约推进状态、下一步动作和跟进记录。
|
||||
2. 已签约执行项目:已经签约,以按 SOP 交付为核心目标,重点展示 SOP 阶段、执行进度、交付物、风险和跟进记录。
|
||||
|
||||
首批运营文件来源:`/Users/mac/天机阁/地阁/慰心斋/3、运营方案`。当前已存在的文件按版本要求补充到对应运营项目,不建立"未归类文件池"。
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 按表格形式展示。
|
||||
- 表格顶部提供项目类型筛选按钮(三选一):
|
||||
- 全部项目
|
||||
- 业务机会项目
|
||||
- 已签约执行项目
|
||||
- 表格字段:
|
||||
- 项目名称(含版本号)
|
||||
- 项目类型
|
||||
- 项目状态
|
||||
- 当前阶段
|
||||
- 交付文件数
|
||||
- 最新跟进
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 用户可以新增运营项目。
|
||||
- 新增项目时必须选择项目类型:业务机会项目或已签约执行项目。
|
||||
- 用户点击表格行后,以抽屉形式展开(720px 宽)。
|
||||
- 抽屉内展示所有字段(失焦自动保存):
|
||||
- 项目名称
|
||||
- 项目版本
|
||||
- 项目状态
|
||||
- 当前阶段
|
||||
- 目标客户
|
||||
- 客户需求
|
||||
- 预计签约金额
|
||||
- 预计签约时间
|
||||
- 签约概率
|
||||
- SOP 阶段
|
||||
- 执行进度
|
||||
- 当前交付物
|
||||
- 风险与阻塞
|
||||
- 下一步动作
|
||||
- 抽屉内支持 Squire 富文本评论,评论支持删除。
|
||||
- 运营文件必须先归属到明确项目版本,再根据该版本要求补充文件。
|
||||
- 已放入运营管理文件夹的文件,需要作为对应项目版本的初始文件导入或索引到运营 Tab。
|
||||
|
||||
#### 业务机会项目 vs 已签约执行项目
|
||||
|
||||
两种项目类型共用同一个抽屉和数据模型,根据 project_type 字段区分:
|
||||
|
||||
- 当 project_type = opportunity 时,优先关注:目标客户、客户需求、预计签约金额、预计签约时间、签约概率、下一步动作。
|
||||
- 当 project_type = execution 时,优先关注:SOP 阶段、执行进度、当前交付物、风险。
|
||||
|
||||
#### 推荐项目类型
|
||||
|
||||
- 业务机会项目(opportunity)
|
||||
- 已签约执行项目(execution)
|
||||
|
||||
#### 业务机会项目推荐状态
|
||||
|
||||
- 线索发现
|
||||
- 需求沟通
|
||||
- 方案准备
|
||||
- 方案已提交
|
||||
- 商务谈判
|
||||
- 待签约
|
||||
- 已签约
|
||||
- 暂缓
|
||||
- 已丢单
|
||||
|
||||
#### 已签约执行项目推荐状态
|
||||
|
||||
- 未启动
|
||||
- SOP 执行中
|
||||
- 有风险
|
||||
- 待客户确认
|
||||
- 已完成
|
||||
- 已归档
|
||||
|
||||
#### SOP 阶段建议
|
||||
|
||||
- 项目启动
|
||||
- 需求确认
|
||||
- 方案确认
|
||||
- 内容生产
|
||||
- 审核确认
|
||||
- 渠道分发
|
||||
- 数据/效果回收
|
||||
- 复盘验收
|
||||
|
||||
---
|
||||
|
||||
### 4.5 产品 Tab
|
||||
|
||||
> **v1.0 变更**:从 v0.9 的"一个版本一个卡片"改为表格布局,保持与销售、业务方案、运营一致的交互模式。
|
||||
|
||||
#### 页面形式
|
||||
|
||||
- 按表格形式展示。
|
||||
- 表格字段:
|
||||
- 产品名称
|
||||
- 版本号
|
||||
- 版本目标
|
||||
- 核心功能
|
||||
- 上线日期
|
||||
- 状态
|
||||
|
||||
#### 交互要求
|
||||
|
||||
- 用户可以新增产品版本。
|
||||
- 用户点击表格行后,以抽屉形式展开(720px 宽)。
|
||||
- 抽屉内展示版本详情(失焦自动保存):
|
||||
- 产品名称
|
||||
- 版本号
|
||||
- 版本目标
|
||||
- 核心功能清单
|
||||
- 上线日期
|
||||
- 当前状态
|
||||
- 备注
|
||||
- 抽屉内支持 Squire 富文本评论,评论支持删除。
|
||||
|
||||
#### 推荐版本状态
|
||||
|
||||
- 规划中
|
||||
- 设计中
|
||||
- 开发中
|
||||
- 测试中
|
||||
- 已上线
|
||||
- 已延期
|
||||
- 已取消
|
||||
|
||||
---
|
||||
|
||||
### 4.6 财务 Tab
|
||||
|
||||
#### 页面结构
|
||||
|
||||
财务 Tab 分为趋势图区域和明细区域。
|
||||
|
||||
#### 趋势图
|
||||
|
||||
需要展示 4 条月度曲线:
|
||||
|
||||
1. 收入按月份的曲线图
|
||||
2. 毛利按月份的曲线图
|
||||
3. 成本/费用按月份的曲线图
|
||||
4. 净利按月份的曲线图
|
||||
|
||||
#### 明细表
|
||||
|
||||
每个指标需要对应明细:
|
||||
|
||||
1. 收入明细
|
||||
2. 毛利明细
|
||||
3. 成本/费用明细
|
||||
4. 净利明细
|
||||
|
||||
#### 推荐财务口径
|
||||
|
||||
- 收入:当月确认收入金额。
|
||||
- 成本/费用:统一录入为成本/费用,再通过类型区分直接成本、期间费用、运营费用、管理费用等。
|
||||
- 毛利:收入 - 成本。
|
||||
- 净利:收入 - 成本/费用。
|
||||
- 月份统计按自然月,月份格式为 YYYY-MM。
|
||||
|
||||
#### 明细字段
|
||||
|
||||
收入明细:
|
||||
|
||||
- 月份
|
||||
- 客户/项目
|
||||
- 收入类型
|
||||
- 金额
|
||||
- 确认日期
|
||||
- 备注
|
||||
|
||||
成本/费用明细:
|
||||
|
||||
- 月份
|
||||
- 客户/项目
|
||||
- 类型
|
||||
- 金额
|
||||
- 发生日期
|
||||
- 备注
|
||||
|
||||
净利明细:
|
||||
|
||||
- 月份
|
||||
- 收入合计
|
||||
- 成本/费用合计
|
||||
- 毛利
|
||||
- 净利
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据对象
|
||||
|
||||
### 5.1 SalesLead 销售线索
|
||||
|
||||
- id
|
||||
- target_customer
|
||||
- priority
|
||||
- status
|
||||
- latest_follow_up_record
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
### 5.2 FollowUpRecord 跟进时间线记录
|
||||
|
||||
- id
|
||||
- target_type(sales / operation / product)
|
||||
- target_id
|
||||
- followed_at
|
||||
- follower
|
||||
- follow_up_method
|
||||
- content(HTML 格式,Squire 生成)
|
||||
- next_action
|
||||
- next_follow_up_at
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
### 5.3 BusinessProposal 业务方案版本
|
||||
|
||||
- id
|
||||
- customer_or_project_name
|
||||
- version
|
||||
- description
|
||||
- status
|
||||
- created_date
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
文件通过 FileAsset 关联(module = "proposal"),按 file_category 分组:
|
||||
- 方案
|
||||
- 成本
|
||||
- SOP
|
||||
- 财务流程
|
||||
|
||||
### 5.4 OperationProject 运营项目
|
||||
|
||||
- id
|
||||
- project_name
|
||||
- project_version
|
||||
- project_type(opportunity / execution)
|
||||
- project_status
|
||||
- current_stage
|
||||
- owner
|
||||
- start_date
|
||||
- end_date
|
||||
- target_customer
|
||||
- customer_need
|
||||
- expected_contract_amount
|
||||
- expected_sign_date
|
||||
- sign_probability
|
||||
- next_action
|
||||
- related_business_proposal_id
|
||||
- sop_file_id
|
||||
- sop_stage
|
||||
- execution_progress
|
||||
- current_deliverable
|
||||
- risks
|
||||
- notes
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
### 5.5 ProductVersion 产品版本
|
||||
|
||||
- id
|
||||
- product_name
|
||||
- version
|
||||
- version_goal
|
||||
- feature_list
|
||||
- launch_date
|
||||
- status
|
||||
- notes
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
### 5.6 FinanceRecord 财务记录
|
||||
|
||||
- id
|
||||
- month(YYYY-MM 格式)
|
||||
- project_name
|
||||
- record_type(revenue / cost_expense)
|
||||
- category
|
||||
- amount
|
||||
- occurred_date
|
||||
- notes
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
### 5.7 FileAsset 文件资产
|
||||
|
||||
- id
|
||||
- module(proposal / operation)
|
||||
- owner_id
|
||||
- owner_version
|
||||
- file_category
|
||||
- file_name
|
||||
- file_type
|
||||
- file_size
|
||||
- file_path
|
||||
- is_external(1=原始索引文件,0=本地上传文件)
|
||||
- notes
|
||||
- created_at
|
||||
- updated_at
|
||||
|
||||
### 5.8 AppScope 系统范围
|
||||
|
||||
- project_name:科普(慰心斋)
|
||||
- user_mode:single_user
|
||||
- project_mode:single_project
|
||||
- multi_user_enabled:false
|
||||
- multi_project_enabled:false
|
||||
|
||||
---
|
||||
|
||||
## 6. 页面与交互规范
|
||||
|
||||
### 6.1 通用能力
|
||||
|
||||
- 所有列表支持新增(内联表单置顶)。
|
||||
- 所有详情通过抽屉展示和编辑。
|
||||
- 所有日期字段使用文本输入。
|
||||
- 所有状态字段使用下拉选择。
|
||||
- 跟进记录统一使用多条时间线记录,Squire 富文本编辑器支持 HTML 格式。
|
||||
- 字段失焦自动保存,保存状态有视觉反馈(保存中…/已保存/保存失败)。
|
||||
- 点击表格行打开抽屉,不需要单独的"详情"按钮。
|
||||
- 保存后列表与详情数据同步更新(自动触发全量 re-render)。
|
||||
|
||||
### 6.2 抽屉交互
|
||||
|
||||
所有 Tab 的详情均使用右侧抽屉。
|
||||
|
||||
- 抽屉固定宽度 720px,Plane 风格紧凑布局。
|
||||
- 字段使用 `drawer-value` 样式:默认透明背景、hover 显示边框、focus 蓝色边框。
|
||||
- 字段失焦自动保存,不需要保存按钮。
|
||||
- 抽屉右上角关闭按钮关闭。
|
||||
- 已删除"点击抽屉外区域自动关闭抽屉"的交互。
|
||||
- 抽屉内支持评论区(Squire 富文本编辑器)。
|
||||
- 评论提交后抽屉保持打开,触发数据刷新。
|
||||
|
||||
### 6.3 文件上传
|
||||
|
||||
业务方案 Tab 和运营 Tab 支持文件上传与在线预览。
|
||||
|
||||
- 上传文件必须归属于某个方案版本和某个要素。
|
||||
- 上传后展示文件名、类型、大小、上传时间。
|
||||
- 文件支持下载(Content-Disposition: attachment)。
|
||||
- 文件支持在线预览(Content-Disposition: inline)。PDF 和图片优先使用浏览器内预览;Word、Excel、PPT 提供下载。
|
||||
- 运营文件必须先归属到明确项目版本,再按该版本要求补充对应文件。
|
||||
- 外部索引文件(慰心斋原始文件)通过 is_external=1 标记,不重复存储。
|
||||
|
||||
### 6.4 评论区
|
||||
|
||||
- 使用 Squire RTE(Fastmail)作为富文本编辑器。
|
||||
- 支持格式:加粗、斜体、下划线、删除线、无序列表、有序列表、引用、撤销、重做。
|
||||
- 工具栏按钮使用 `onmousedown` 事件,防止点击按钮时编辑器失焦。
|
||||
- 评论 HTML 用 `encodeURIComponent` 存入 `data-html` 属性,渲染时 `decodeURIComponent` 解析到 `.rich-content` 容器。
|
||||
- 评论内容区通过 `.rich-content ul/ol { list-style: revert; }` 覆盖 Tailwind CSS reset,确保列表编号/圆点正常显示。
|
||||
- 所有评论记录均有删除图标(hover 时显示),点击后弹窗确认删除。
|
||||
|
||||
---
|
||||
|
||||
## 7. 技术栈与实现约束
|
||||
|
||||
### 7.1 前端技术栈
|
||||
|
||||
OPC 工作台前端采用:
|
||||
|
||||
- Jinja2:服务端渲染单一页面模板(`templates/index.html`)。
|
||||
- Tailwind CSS CDN:样式系统、布局、间距、颜色和响应式规则,不使用 Vite 构建链。
|
||||
- 原生 JavaScript(`static/app.js`,约 465 行):Tab 切换、抽屉详情、表单提交、文件上传、数据刷新、自动保存。
|
||||
- 自定义 CSS(`static/styles.css`,约 492 行):Drawer 样式、Squire 编辑器样式、表格行 hover、状态 badge、评论区、活动时间线。
|
||||
- Chart.js CDN 4.4.8(chart.js):财务趋势折线图(首页 + 财务 Tab 各一个),配置 `maintainAspectRatio: false` + 固定容器高度。
|
||||
- Lucide(unpkg CDN):线性图标库,用于导航、操作按钮、文件预览/下载、属性标签等,尺寸 16px。
|
||||
- Squire RTE CDN v1(`build/squire-raw.js`):富文本评论编辑器,支持 HTML 格式。
|
||||
|
||||
### 7.2 后端技术栈
|
||||
|
||||
OPC 工作台后端采用:
|
||||
|
||||
- Flask 3.1.3:页面渲染(Jinja2 模板)、REST API、文件上传、文件访问、在线预览入口和静态资源服务。
|
||||
- sqlite3(Python 标准库,无 ORM):SQLite 数据读写。
|
||||
- Flask development server:本地单用户运行,端口 5177,debug 模式。
|
||||
|
||||
### 7.3 数据库与存储
|
||||
|
||||
首版数据库采用 SQLite。
|
||||
|
||||
- 主数据库文件:`data/opc.sqlite`(已在 .gitignore 中排除)。
|
||||
- 原财务 manager 的 SQLite 数据需要迁移或合并到 OPC 主数据库,不再作为独立系统运行。
|
||||
- SQLite 中存储销售、跟进时间线、业务方案版本、运营项目、文件元数据、产品版本、财务记录等结构化数据。
|
||||
- 上传文件本体存储在本地文件系统(`data/uploads/`),不直接写入 SQLite。
|
||||
- 外部引用文件(慰心斋原始文件)通过 file_assets 表的 is_external 字段标记,路径指向原始文件位置。
|
||||
- SQLite 仅保存文件元数据,包括文件名、类型、大小、所属模块、所属项目/版本、文件路径、上传时间。
|
||||
- 后端提供文件下载和在线预览访问 URL(`/api/files/<id>/content?inline=true|false`)。
|
||||
|
||||
### 7.4 财务 manager 合并策略
|
||||
|
||||
原财务 manager 系统位于 `/Users/mac/天机阁/地阁/慰心斋/5、财务管理/mananger`。
|
||||
|
||||
合并要求:
|
||||
|
||||
- 不保留原财务 manager 作为独立入口。
|
||||
- 复用或迁移原财务数据结构中的客户、回款、成本、文件索引等有价值数据。
|
||||
- 将原财务 manager 的财务能力合并到 OPC 的财务 Tab。
|
||||
- 财务数据最终统一进入 OPC 主数据库。
|
||||
- 如存在字段差异,以 OPC v1.0 的 FinanceRecord 和财务口径为准。
|
||||
|
||||
### 7.5 API 设计原则
|
||||
|
||||
- API 采用 REST 风格。
|
||||
- `/api/bootstrap`:一次性加载所有模块数据 + 首页聚合指标 + 财务月度汇总。
|
||||
- 所有业务对象提供基础 CRUD 接口(`/api/<resource>` + `/api/<resource>/<id>`)。
|
||||
- 文件上传接口通过 FormData 接收 file + module + owner_id + owner_version + file_category。
|
||||
- 财务汇总接口由后端根据明细计算收入、毛利、成本/费用、净利月度数据。
|
||||
- 首页指标接口由后端聚合销售、运营、产品、财务数据生成。
|
||||
- `/api/followups/<target_type>/<target_id>`:新增评论。
|
||||
- `/api/followups/<id>`:删除评论。
|
||||
- 首版不做登录、鉴权、多用户权限和租户隔离。
|
||||
|
||||
### 7.6 项目结构
|
||||
|
||||
```
|
||||
OPC-Manager/
|
||||
├── backend/
|
||||
│ ├── flask_app.py # Flask 主应用(~480 行)
|
||||
│ └── requirements.txt
|
||||
├── templates/
|
||||
│ └── index.html # Jinja2 页面模板
|
||||
├── static/
|
||||
│ ├── app.js # 原生 JS 页面交互(~465 行)
|
||||
│ └── styles.css # 自定义样式(~492 行)
|
||||
├── data/
|
||||
│ ├── opc.sqlite # SQLite 数据库(gitignore)
|
||||
│ └── uploads/ # 上传文件目录(gitignore)
|
||||
├── docs/
|
||||
│ ├── design-system-summary.md
|
||||
│ └── frontend-design-checklist.md
|
||||
├── scripts/
|
||||
│ └── deploy.sh # 标准部署脚本
|
||||
├── VERSION_LOG.md # 版本发布日志
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 7.7 开发约束
|
||||
|
||||
- 不使用 Ant Design、React、Vite、shadcn/ui 等重型框架。
|
||||
- 不新增多套 UI 组件体系。
|
||||
- Tailwind CSS CDN 承接设计系统中的颜色、间距、字号、圆角、阴影和状态表达。
|
||||
- 不使用 PostgreSQL、MySQL 等独立数据库服务。
|
||||
- 不修改 AiMaMi 本地代理配置。
|
||||
- 不使用 destructive git 命令。
|
||||
- 不提交 `data/opc.sqlite` 到仓库。
|
||||
- 修改代码后必须运行:`node --check static/app.js` + `curl /api/health`。
|
||||
- 服务启动前先清理 5177 端口占用。
|
||||
|
||||
---
|
||||
|
||||
## 8. 设计规范
|
||||
|
||||
### 8.1 设计目标
|
||||
|
||||
OPC 工作台采用企业级经营管理后台风格,重点是清晰、稳定、可扫描、适合会议复盘。不做营销页风格,不使用过度装饰、强动效或大面积渐变。
|
||||
|
||||
### 8.2 视觉规范
|
||||
|
||||
| 要素 | 规范 |
|
||||
|------|------|
|
||||
| 主色 | `#2563eb` (blue-600) / `#1d4ed8` (blue-700) |
|
||||
| 背景 | `bg-slate-50` 浅灰 |
|
||||
| 卡片/表格底色 | `white` + `border-slate-200` 细边框 |
|
||||
| 风险状态 | 红 `#fef2f2` / `#b91c1c` |
|
||||
| 待处理状态 | 琥珀 `#fffbeb` / `#b45309` |
|
||||
| 完成状态 | 绿 `#ecfdf5` / `#047857` |
|
||||
| 信息状态 | 蓝 `#eff6ff` / `#1d4ed8` |
|
||||
| 归档/默认 | 灰 `#f1f5f9` / `#475569` |
|
||||
| 圆角 | 表格 8px,按钮 6px,badge 全圆 |
|
||||
| 表格行 hover | `#f1f5f9` 浅灰背景 |
|
||||
| 字段编辑态 | 透明→hover 浅灰→focus 蓝边框 |
|
||||
|
||||
### 8.3 组件风格要求
|
||||
|
||||
- 销售管理:表格 + 状态标签 + 抽屉详情。
|
||||
- 业务方案:表格 + 状态标签 + 文件分组(抽屉内 4 组文件上传/预览/下载)。
|
||||
- 运营:表格 + 项目类型筛选 + 抽屉详情。
|
||||
- 产品:表格 + 状态标签 + 抽屉详情。
|
||||
- 财务:月度趋势折线图 + 明细表。
|
||||
|
||||
### 8.4 设计辅助工具(已使用)
|
||||
|
||||
v1.0 开发前使用了以下设计辅助工具:
|
||||
|
||||
- `ui-ux-pro-max-skill`:生成设计系统摘要(`docs/design-system-summary.md`),定义色彩、布局、组件规则。
|
||||
- `frontend-design`:生成页面级 UI 落地清单(`docs/frontend-design-checklist.md`),定义各页面的检查要点和 Post-Development Audit 标准。
|
||||
|
||||
实际落地采用 Flask + Jinja2 + Tailwind CSS CDN + Lucide + Chart.js + 原生 JavaScript + SQLite 技术栈,设计规范作为指导但以代码实现为准。
|
||||
|
||||
---
|
||||
|
||||
## 9. 首页指标要求
|
||||
|
||||
v1.0 首页展示 7 项关键指标(4 列网格自动换行):
|
||||
|
||||
- P0 客户数
|
||||
- 跟进中销售机会数
|
||||
- 已签约执行项目数
|
||||
- 有风险项目数
|
||||
- 本月收入
|
||||
- 本月净利
|
||||
- 即将上线产品版本数
|
||||
|
||||
首页作为默认进入页,点击指标可跳转到对应 Tab。
|
||||
|
||||
---
|
||||
|
||||
## 10. 非功能需求
|
||||
|
||||
- 数据应持久化保存,刷新页面不丢失。
|
||||
- 财务数据计算结果应与明细一致。
|
||||
- 文件上传后应可追溯到对应业务方案版本。
|
||||
- 运营文件上传后应可追溯到对应项目版本。
|
||||
- 页面应支持桌面端优先使用(最小宽度 1180px)。
|
||||
- 表格内容应清晰紧凑,适合会议和经营复盘场景。
|
||||
- 系统应保留后续复用到其他业务单元的扩展能力。
|
||||
- 前端实现必须符合 Jinja2 + Tailwind CSS CDN + Lucide + Chart.js + Squire + 原生 JavaScript 技术栈约束。
|
||||
- 后端实现必须符合 Flask + sqlite3 + SQLite 技术栈约束。
|
||||
- 上传文件必须采用本地文件系统存储,SQLite 存储文件元数据。
|
||||
- 首版为单用户、单项目版本,项目名称为科普(慰心斋)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 验收标准
|
||||
|
||||
### 首页
|
||||
|
||||
- 默认进入系统后展示全局首页。
|
||||
- 首页展示 7 项关键指标(4 列网格)。
|
||||
- 首页展示财务趋势图、风险提醒和近期动态。
|
||||
- 点击首页指标可以跳转到对应 Tab。
|
||||
|
||||
### 销售管理
|
||||
|
||||
- 可以新增销售客户。
|
||||
- 可以在表格中看到目标客户、优先级、状态、最新跟进记录。
|
||||
- 点击行后可以打开抽屉查看和编辑详情(失焦自动保存)。
|
||||
- 销售跟进记录以多条时间线形式展示,支持 Squire 富文本评论和删除。
|
||||
|
||||
### 业务方案
|
||||
|
||||
- 可以新增方案版本。
|
||||
- 点击行后打开抽屉,展示属性和 4 个文件分组(方案/成本/SOP/财务流程)。
|
||||
- 每个分组可以上传多个文件。
|
||||
- 上传后文件能够在线预览和下载。
|
||||
- 抽屉内支持评论。
|
||||
|
||||
### 运营
|
||||
|
||||
- 可以新增运营项目。
|
||||
- 可以将运营项目分为业务机会项目和已签约执行项目。
|
||||
- 可以在表格中看到项目名称、类型、状态、当前阶段、文件数、最新跟进。
|
||||
- 顶部提供项目类型筛选按钮(全部/业务机会/已签约执行)。
|
||||
- 点击项目后可以用抽屉查看和编辑详情。
|
||||
- 抽屉内支持评论。
|
||||
|
||||
### 产品
|
||||
|
||||
- 可以新增产品版本。
|
||||
- 表格展示产品名称、版本号、版本目标、核心功能、上线日期、状态。
|
||||
- 点击行后可以用抽屉查看和编辑版本详情。
|
||||
- 抽屉内支持评论。
|
||||
|
||||
### 财务
|
||||
|
||||
- 可以录入收入、成本/费用明细。
|
||||
- 系统按月份展示收入、毛利、成本/费用、净利曲线(首页 + 财务 Tab 各一个)。
|
||||
- 系统可以展示对应明细。
|
||||
- 月份按自然月统计,格式为 YYYY-MM。
|
||||
- 成本和费用统一作为成本/费用录入,再通过类型区分。
|
||||
- 净利 = 收入 - 成本/费用。
|
||||
|
||||
### UI/UX
|
||||
|
||||
- 前端页面基于 Jinja2 + Tailwind CSS CDN + 原生 JavaScript 实现。
|
||||
- 图标库基于 Lucide 浏览器版实现。
|
||||
- 富文本评论基于 Squire RTE 实现。
|
||||
- 财务趋势图基于 Chart.js 实现。
|
||||
- 页面符合企业级经营管理后台风格,信息清晰、表格紧凑、状态可扫描。
|
||||
- 销售、运营、产品、业务方案详情通过 720px 抽屉承载。
|
||||
- 字段失焦自动保存,有状态反馈。
|
||||
- 财务图表与明细表可对应核对。
|
||||
|
||||
### 后端与数据
|
||||
|
||||
- 后端基于 Flask 实现页面渲染、REST API 和文件服务。
|
||||
- 数据读写基于 sqlite3 实现。
|
||||
- 数据持久化使用 SQLite,主数据库为 OPC 主库。
|
||||
- 原财务 manager 不再作为独立系统运行,其能力合并进 OPC 财务 Tab。
|
||||
- 文件本体存储在本地 uploads 目录,SQLite 存储文件元数据。
|
||||
- 后端提供文件上传、下载和在线预览访问入口。
|
||||
- 首页指标和财务月度曲线由后端基于明细数据计算生成。
|
||||
|
||||
---
|
||||
|
||||
## 12. 版本历史
|
||||
|
||||
| 版本 | 日期 | 阶段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| v0.9 | 2026-05-29 | 需求核对稿 | 初始 PRD,定义 6 Tab 功能架构、数据模型、技术选型 |
|
||||
| v1.0 | 2026-06-01 | 实际落地版 | 匹配 OPC-Manager v1.0.2 代码实现,修正业务方案/产品从卡片改表格,补充 Squire 编辑器、抽屉交互等实际落地细节 |
|
||||
|
||||
详细修改日志见 `CHANGELOG.md`。
|
||||
84
scripts/deploy.sh
Executable file
84
scripts/deploy.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# OPC Manager 标准部署脚本
|
||||
# 用法: ./scripts/deploy.sh <version> [message]
|
||||
# 示例: ./scripts/deploy.sh v0.1.1 "修复首页样式"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-}"
|
||||
MESSAGE="${2:-Release $VERSION}"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "用法: ./scripts/deploy.sh <version> [message]"
|
||||
echo "示例: ./scripts/deploy.sh v0.1.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="opc-manager-$VERSION"
|
||||
REMOTE_HOST="${OPC_REMOTE_HOST:-business}"
|
||||
REMOTE_PATH="${OPC_REMOTE_PATH:-/opt/opc-manager}"
|
||||
SERVICE_NAME="${OPC_SERVICE_NAME:-opc-manager.service}"
|
||||
HEALTH_URL="${OPC_HEALTH_URL:-https://opc.yxcowork.vip/api/health}"
|
||||
HOME_URL="${OPC_HOME_URL:-https://opc.yxcowork.vip}"
|
||||
|
||||
echo "=== OPC Manager 部署 $VERSION ==="
|
||||
|
||||
# 1. 确保在项目根目录
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# 2. 更新 VERSION_LOG.md
|
||||
echo "" >> VERSION_LOG.md
|
||||
echo "## $TAG - $(date +%F)" >> VERSION_LOG.md
|
||||
echo "" >> VERSION_LOG.md
|
||||
echo "- $MESSAGE" >> VERSION_LOG.md
|
||||
echo "" >> VERSION_LOG.md
|
||||
echo "VERSION_LOG.md 已更新"
|
||||
|
||||
# 3. Git commit
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "没有需要提交的变更,跳过 commit"
|
||||
else
|
||||
git commit -m "release: $TAG - $MESSAGE"
|
||||
echo "已提交: release: $TAG"
|
||||
fi
|
||||
|
||||
# 4. Git tag(轻量 tag,只打在 OPC 独立仓库)
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "Tag $TAG 已存在,跳过"
|
||||
else
|
||||
git tag "$TAG"
|
||||
echo "已打 tag: $TAG"
|
||||
fi
|
||||
|
||||
# 5. Git push
|
||||
echo "推送代码和 tag..."
|
||||
git push origin main
|
||||
git push origin "$TAG"
|
||||
|
||||
# 6. 服务器同步
|
||||
echo "同步到服务器 $REMOTE_HOST..."
|
||||
ssh "$REMOTE_HOST" "cd $REMOTE_PATH && git fetch origin && git reset --hard origin/main"
|
||||
|
||||
# 7. 重启服务
|
||||
echo "重启 $SERVICE_NAME..."
|
||||
ssh "$REMOTE_HOST" "sudo systemctl restart $SERVICE_NAME && sleep 2"
|
||||
ssh "$REMOTE_HOST" "systemctl status $SERVICE_NAME --no-pager" | head -6
|
||||
|
||||
# 8. 健康检查
|
||||
echo ""
|
||||
echo "健康检查..."
|
||||
sleep 2
|
||||
HEALTH_RESULT=$(curl -s "$HEALTH_URL")
|
||||
echo "$HEALTH_RESULT"
|
||||
|
||||
HOME_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$HOME_URL")
|
||||
echo "首页 HTTP 状态: $HOME_CODE"
|
||||
|
||||
if [ "$HOME_CODE" = "200" ]; then
|
||||
echo ""
|
||||
echo "=== 部署 $VERSION 完成 ==="
|
||||
else
|
||||
echo ""
|
||||
echo "!!! 警告:首页返回 $HOME_CODE,请检查 !!!"
|
||||
fi
|
||||
166
static/app.js
166
static/app.js
@@ -3,6 +3,8 @@ const state = {
|
||||
data: null,
|
||||
opFilter: "all",
|
||||
chart: null,
|
||||
chart2: null,
|
||||
productPlatform: "all",
|
||||
};
|
||||
|
||||
const money = (value) => `${Number(value || 0).toLocaleString("zh-CN")} 万`;
|
||||
@@ -65,6 +67,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 +96,7 @@ function renderHome() {
|
||||
const m = summary.metrics;
|
||||
document.querySelector("#home").innerHTML = `
|
||||
<div class="grid gap-5">
|
||||
<div class="grid grid-cols-7 gap-3">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
${[
|
||||
["P0 客户数", m.p0_customers, "sales"],
|
||||
["跟进中销售机会", m.active_sales, "sales"],
|
||||
@@ -81,13 +105,24 @@ function renderHome() {
|
||||
["本月收入", money(m.monthly_revenue), "finance"],
|
||||
["本月净利", money(m.monthly_net_profit), "finance"],
|
||||
["即将上线版本", m.upcoming_products, "products"],
|
||||
["已签约未执行", money(m.signed_not_executed), "finance"],
|
||||
].map(([label, value, tab]) => `<button class="metric-card" onclick="switchTab('${tab}')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="gauge"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></button>`).join("")}
|
||||
</div>
|
||||
<div class="grid grid-cols-[1.35fr_0.65fr] gap-5">
|
||||
${card(`<div class="mb-4 flex items-center justify-between"><h2 class="text-lg font-bold">财务趋势</h2>${badge("YYYY-MM")}</div><canvas id="financeChart" height="125"></canvas>`, "p-5")}
|
||||
${card(`<h2 class="text-lg font-bold">风险提醒</h2><div class="mt-3 grid gap-2">${(summary.risks.length ? summary.risks : [{ title: "暂无高风险", content: "当前无明确阻塞,按周更新即可。" }]).map((r) => `<div class="rounded-md border border-amber-200 bg-amber-50 p-3"><p class="font-bold text-amber-900">${r.title}</p><p class="mt-1 text-sm text-amber-800">${r.content}</p></div>`).join("")}</div>`, "p-5")}
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
${[
|
||||
["已签约合同总额", money(m.signed_amount), "operations"],
|
||||
["合同流程中", money(m.pipeline_amount), "operations"],
|
||||
["年度累计确收", money(m.revenue_annual), "finance"],
|
||||
["Q2 累计确收", money(m.revenue_q2), "finance"],
|
||||
["年度累计毛利", money(m.gross_annual), "finance"],
|
||||
["Q2 累计毛利", money(m.gross_q2), "finance"],
|
||||
].map(([label, value, tab]) => `<button class="metric-card" onclick="switchTab('${tab}')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="trending-up"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></button>`).join("")}
|
||||
</div>
|
||||
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex justify-between rounded-md bg-slate-50 px-3 py-2 text-sm"><span>${r.content}</span><span class="text-slate-500">${r.followed_at}</span></div>`).join("")}</div>`, "p-5")}
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">财务趋势</h2>${badge("YYYY-MM")}</div><div style="position:relative;height:140px"><canvas id="financeChart"></canvas></div>`, "p-4")}
|
||||
${card(`<h2 class="text-lg font-bold">风险提醒</h2><div class="mt-3 grid gap-2">${(summary.risks.length ? summary.risks : [{ title: "暂无高风险", content: "当前无明确阻塞,按周更新即可。" }]).map((r) => `<div class="rounded-md border border-amber-200 bg-amber-50 p-3"><p class="font-bold text-amber-900">${r.title}</p><p class="mt-1 text-sm text-amber-800 break-words">${r.content}</p></div>`).join("")}</div>`, "p-5")}
|
||||
</div>
|
||||
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex justify-between rounded-md bg-slate-50 px-3 py-2 text-sm"><span class="break-words">${r.content}</span><span class="text-slate-500">${r.followed_at}</span></div>`).join("")}</div>`, "p-5")}
|
||||
</div>
|
||||
`;
|
||||
renderChart(financeMonthly);
|
||||
@@ -108,7 +143,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 } } } } },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,10 +156,15 @@ function formHtml(fields, button) {
|
||||
|
||||
async function createResource(event, resource) {
|
||||
event.preventDefault();
|
||||
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
|
||||
await api(`/api/${resource}`, { method: "POST", body: JSON.stringify({ data }) });
|
||||
event.currentTarget.reset();
|
||||
await load();
|
||||
const form = event.currentTarget;
|
||||
const data = Object.fromEntries(new FormData(form).entries());
|
||||
try {
|
||||
await api(`/api/${resource}`, { method: "POST", body: JSON.stringify({ data }) });
|
||||
form.reset();
|
||||
await load();
|
||||
} catch (error) {
|
||||
alert("创建失败:" + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
window.createSales = (event) => createResource(event, "sales");
|
||||
@@ -139,11 +179,11 @@ function renderSales() {
|
||||
const salesClicks = state.data.sales.map((x) => ({ resource: "sales", id: x.id }));
|
||||
document.querySelector("#sales").innerHTML = `<div class="grid gap-4">
|
||||
${card(formHtml([
|
||||
{ label: "目标客户", input: `<input name="target_customer" required placeholder="客户名称">` },
|
||||
{ label: "业务机会", input: `<input name="target_customer" required placeholder="客户名称">` },
|
||||
{ label: "优先级", input: `<select name="priority"><option>P0</option><option selected>P1</option><option>P2</option><option>P3</option></select>` },
|
||||
{ label: "状态", input: `<select name="status"><option>待跟进</option><option>跟进中</option><option>方案中</option><option>商务谈判</option><option>已签约</option><option>暂缓</option><option>已丢单</option></select>` },
|
||||
], { handler: "createSales", text: `<i data-lucide="plus"></i>新增客户` }), "p-4")}
|
||||
${renderTable(["目标客户", "优先级", "状态", "最新跟进记录"], rows, salesClicks)}
|
||||
], { handler: "createSales", text: `<i data-lucide="plus"></i>新增业务机会` }), "p-4")}
|
||||
${renderTable(["业务机会", "优先级", "状态", "最新跟进记录"], rows, salesClicks)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -194,7 +234,7 @@ window.uploadFile = async (event, module, ownerId, version, category) => {
|
||||
|
||||
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) => [`<strong>${x.project_name}</strong><p class="text-xs text-slate-500">${x.project_version}</p>`, badge(x.project_type), badge(x.project_status), text(x.current_stage || x.sop_stage), `${x.files.length} 个`, text(x.latest_follow_up_record)]);
|
||||
const opRows = items.map((x) => [`<strong>${x.project_name}</strong><p class="text-xs text-slate-500">${x.project_version}</p>`, 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 = `<div class="grid gap-4">
|
||||
${card(formHtml([
|
||||
@@ -204,26 +244,29 @@ function renderOperations() {
|
||||
{ label: "状态", input: `<input name="project_status" value="线索发现">` },
|
||||
], { handler: "createOperation", text: "新增项目" }), "p-4")}
|
||||
<div class="flex gap-2">${[["all","全部项目"],["opportunity","业务机会项目"],["execution","已签约执行项目"]].map(([k,v]) => `<button class="btn ${state.opFilter === k ? "btn-primary" : "btn-ghost"}" onclick="state.opFilter='${k}'; renderOperations()">${v}</button>`).join("")}</div>
|
||||
${renderTable(["项目名称", "类型", "状态", "当前阶段", "交付文件", "最新跟进"], opRows, opClicks)}
|
||||
${renderTable(["项目名称", "类型", "状态", "金额", "当前阶段", "交付文件", "最新跟进"], opRows, opClicks)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderProducts() {
|
||||
const items = state.productPlatform === "all" ? state.data.products : state.data.products.filter((x) => (x.platform || "") === state.productPlatform);
|
||||
document.querySelector("#products").innerHTML = `<div class="grid gap-4">
|
||||
${card(formHtml([
|
||||
{ label: "产品名称", input: `<input name="product_name" required>` },
|
||||
{ label: "版本号", input: `<input name="version" required>` },
|
||||
{ label: "平台", input: `<select name="platform"><option value="">—</option><option>真研平台</option><option>科普平台</option><option>关爱平台</option></select>` },
|
||||
{ label: "上线日期", input: `<input name="launch_date" placeholder="2026-Q3">` },
|
||||
{ label: "状态", input: `<select name="status"><option>规划中</option><option>设计中</option><option>开发中</option><option>测试中</option><option>已上线</option><option>已延期</option><option>已取消</option></select>` },
|
||||
], { handler: "createProduct", text: "新增版本" }), "p-4")}
|
||||
${renderTable(["产品名称", "版本号", "版本目标", "核心功能", "上线日期", "状态"], state.data.products.map((p) => [p.product_name, p.version, text(p.version_goal), text(p.feature_list), text(p.launch_date), badge(p.status)]), state.data.products.map((p) => ({ resource: "products", id: p.id })))}
|
||||
<div class="flex gap-2">${[["all","全部"],["真研平台","真研"],["科普平台","科普"],["关爱平台","关爱"]].map(([k,v]) => `<button class="btn ${state.productPlatform === k ? "btn-primary" : "btn-ghost"}" onclick="state.productPlatform='${k}'; renderProducts()">${v}</button>`).join("")}</div>
|
||||
${renderTable(["产品名称", "版本号", "版本目标", "核心功能", "平台", "上线日期", "状态"], items.map((p) => [p.product_name, p.version, text(p.version_goal), text(p.feature_list), text(p.platform), text(p.launch_date), badge(p.status)]), items.map((p) => ({ resource: "products", id: p.id })))}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderFinance() {
|
||||
const rows = state.data.finance.map((x) => [x.month, badge(x.record_type === "revenue" ? "收入" : "成本/费用"), x.category, money(x.amount), x.occurred_date, text(x.notes)]);
|
||||
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
||||
${card(`<h2 class="mb-4 text-lg font-bold">收入、毛利、成本/费用、净利月度曲线</h2><canvas id="financeChart2" height="105"></canvas>`, "p-5")}
|
||||
${card(`<h2 class="mb-4 text-lg font-bold">收入、毛利、成本/费用、净利月度曲线</h2><div style="position:relative;height:300px"><canvas id="financeChart2"></canvas></div>`, "p-5")}
|
||||
${card(formHtml([
|
||||
{ label: "月份", input: `<input name="month" required placeholder="YYYY-MM" pattern="\\d{4}-\\d{2}">` },
|
||||
{ label: "类型", input: `<select name="record_type"><option value="revenue">收入</option><option value="cost_expense">成本/费用</option></select>` },
|
||||
@@ -239,7 +282,8 @@ function renderFinance() {
|
||||
function renderChartOn(id, data) {
|
||||
const canvas = document.querySelector(`#${id}`);
|
||||
if (!canvas || !window.Chart) return;
|
||||
new Chart(canvas, {
|
||||
if (state.chart2) state.chart2.destroy();
|
||||
state.chart2 = new Chart(canvas, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: data.map((x) => x.month),
|
||||
@@ -250,7 +294,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 } } } } },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,19 +314,19 @@ function openDrawer(resource, id) {
|
||||
const item = list.find((x) => x.id === id);
|
||||
const drawer = document.querySelector("#drawer");
|
||||
const fields = resource === "sales"
|
||||
? [["target_customer","目标客户"],["priority","优先级"],["status","状态"]]
|
||||
? [["target_customer","业务机会"],["priority","优先级"],["status","状态"]]
|
||||
: resource === "operations"
|
||||
? [["project_name","项目名称"],["project_version","项目版本"],["project_status","项目状态"],["current_stage","当前阶段"],["target_customer","目标客户"],["customer_need","客户需求"],["expected_contract_amount","预计签约金额"],["expected_sign_date","预计签约时间"],["sign_probability","签约概率"],["sop_stage","SOP 阶段"],["execution_progress","执行进度"],["current_deliverable","当前交付物"],["risks","风险与阻塞"],["next_action","下一步动作"]]
|
||||
? [["project_name","项目名称"],["project_version","项目版本"],["project_status","项目状态"],["current_stage","当前阶段"],["target_customer","业务机会"],["customer_need","客户需求"],["expected_contract_amount","预计签约金额"],["expected_sign_date","预计签约时间"],["sign_probability","签约概率"],["sop_stage","SOP 阶段"],["execution_progress","执行进度"],["current_deliverable","当前交付物"],["risks","风险与阻塞"],["next_action","下一步动作"]]
|
||||
: resource === "proposals"
|
||||
? [["customer_or_project_name","客户/项目"],["version","版本号"],["description","版本说明"],["created_date","创建日期"],["status","状态"]]
|
||||
: [["product_name","产品名称"],["version","版本号"],["version_goal","版本目标"],["feature_list","核心功能清单"],["launch_date","上线日期"],["status","当前状态"],["notes","备注"]];
|
||||
: [["product_name","产品名称"],["version","版本号"],["version_goal","版本目标"],["feature_list","核心功能清单"],["platform","平台"],["launch_date","上线日期"],["status","当前状态"],["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",
|
||||
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",
|
||||
product_name: "box", version: "tag", version_goal: "target", feature_list: "list", platform: "layers",
|
||||
launch_date: "calendar", notes: "sticky-note"
|
||||
};
|
||||
const multilineFields = ["customer_need", "current_deliverable", "risks", "next_action", "version_goal", "feature_list", "notes"];
|
||||
@@ -296,12 +340,24 @@ function openDrawer(resource, id) {
|
||||
${resource === "proposals" ? `<section><h3 class="drawer-section-title">方案文件</h3><div class="grid gap-2">${["方案","成本","SOP","财务流程"].map((cat) => fileGroup("proposal", item.id, item.version, cat, item.files.filter((f) => f.file_category === cat))).join("")}</div></section>` : ""}
|
||||
${followupTarget ? `<section>
|
||||
<h3 class="drawer-section-title">活动 / 跟进</h3>
|
||||
<div class="grid gap-2">${(item.followups || []).map((f) => `<div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span>${f.follower} · ${f.follow_up_method}</span><span>${f.followed_at}</span></div><div class="mt-1 leading-5 text-slate-800 trix-content">${f.content}</div>${f.next_action ? `<p class="mt-1 leading-5 text-blue-700">下一步:${text(f.next_action)}</p>` : ""}</div><button class="activity-delete" onclick="deleteFollowup(event, ${f.id}, '${resource}', ${item.id})" title="删除评论"><i data-lucide="trash-2"></i></button></div>`).join("")}</div>
|
||||
<div class="grid gap-2">${(item.followups || []).map((f) => `<div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span>${f.follower} · ${f.follow_up_method}</span><span>${f.followed_at}</span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html="${encodeURIComponent(f.content || '')}"></div>${f.next_action ? `<p class="mt-1 leading-5 text-blue-700">下一步:${text(f.next_action)}</p>` : ""}</div><button class="activity-delete" onclick="deleteFollowup(event, ${f.id}, '${resource}', ${item.id})" title="删除评论"><i data-lucide="trash-2"></i></button></div>`).join("")}</div>
|
||||
<form class="comment-box mt-3" onsubmit="submitComment(event,'${followupTarget}',${item.id},'${resource}')">
|
||||
<input id="commentHidden_${resource}_${item.id}" type="hidden" name="content">
|
||||
<trix-editor input="commentHidden_${resource}_${item.id}" placeholder="添加评论" class="comment-trix"></trix-editor>
|
||||
<div class="squire-toolbar">
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('bold')" title="加粗"><i data-lucide="bold"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('italic')" title="斜体"><i data-lucide="italic"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('underline')" title="下划线"><i data-lucide="underline"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('strikethrough')" title="删除线"><i data-lucide="strikethrough"></i></button>
|
||||
<span class="squire-sep"></span>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('makeUnorderedList')" title="无序列表"><i data-lucide="list"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('makeOrderedList')" title="有序列表"><i data-lucide="list-ordered"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('blockquote')" title="引用"><i data-lucide="quote"></i></button>
|
||||
<span class="squire-sep"></span>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('undo')" title="撤销"><i data-lucide="undo"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('redo')" title="重做"><i data-lucide="redo"></i></button>
|
||||
</div>
|
||||
<div class="squire-editor" id="squire_${resource}_${item.id}" placeholder="添加评论"></div>
|
||||
<div class="comment-toolbar">
|
||||
<span class="comment-hint">支持 Markdown 格式</span>
|
||||
<span class="comment-hint">支持富文本编辑</span>
|
||||
<button class="btn btn-primary btn-sm comment-submit" type="submit">评论</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -310,6 +366,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 +430,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 === "<div><br></div>" || content === "<p><br></p>") return;
|
||||
const button = form.querySelector(".comment-submit");
|
||||
button.disabled = true;
|
||||
button.textContent = "发送中…";
|
||||
@@ -374,10 +476,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
static/vendor/chart.js
vendored
Normal file
20
static/vendor/chart.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
static/vendor/lucide.js
vendored
Normal file
12
static/vendor/lucide.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5015
static/vendor/squire.js
vendored
Normal file
5015
static/vendor/squire.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
83
static/vendor/tailwind.js
vendored
Normal file
83
static/vendor/tailwind.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>科普(慰心斋)OPC 工作台</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="{{ url_for('static', filename='vendor/tailwind.js') }}"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
@@ -21,10 +21,9 @@
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trix@2/dist/trix.css">
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/trix@2/dist/trix.umd.min.js" defer></script>
|
||||
<script src="{{ url_for('static', filename='vendor/chart.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='vendor/squire.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='vendor/lucide.js') }}" defer></script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-50 text-slate-950">
|
||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||
@@ -37,7 +36,7 @@
|
||||
|
||||
<nav class="tabs border-b border-slate-200 bg-white px-8" id="tabs">
|
||||
<button class="active" data-tab="home"><i data-lucide="home"></i>首页</button>
|
||||
<button data-tab="sales"><i data-lucide="briefcase-business"></i>销售管理</button>
|
||||
<button data-tab="sales"><i data-lucide="briefcase-business"></i>业务机会</button>
|
||||
<button data-tab="proposals"><i data-lucide="file-text"></i>业务方案</button>
|
||||
<button data-tab="operations"><i data-lucide="activity"></i>运营管理</button>
|
||||
<button data-tab="products"><i data-lucide="package"></i>产品研发</button>
|
||||
|
||||
Reference in New Issue
Block a user