Files
erp/docs/superpowers/specs/2026-04-20-freelance-itops-plugin-enhancement-design.md
iven bcc6662add
Some checks failed
CI / frontend-build (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs(spec): 修复 spec 审查问题 — cascade/validation 标注为修改已有字段,dashboard 标注平台依赖
- CRITICAL: 明确 dashboard widgets 需要平台 manifest.rs 扩展
- HIGH: cascade/validation/visible_when 均改为"已有字段追加属性"
- HIGH: visible_when 提供完整 TOML 语法
- MEDIUM: 模板引擎说明(Handlebars)和关系解析机制
- MEDIUM: itops validation 补充字段上下文
- MEDIUM: itops dashboard 明确插入位置
2026-04-20 00:50:29 +08:00

20 KiB
Raw Blame History

freelance + itops 插件增强设计规格

日期: 2026-04-20 来源: 多专家头脑风暴UX专家 + 业务顾问 + 运维专家 + 财务专家) 状态: Draft 前置: docs/superpowers/specs/2026-04-19-shantou-zhijie-it-services-plugins-design.md


1. 背景与动机

当前插件是「数据录入系统」,不是「赚钱工具」。一人 IT 服务公司的核心痛点:

  1. 钱从哪里来? — 商机跟进靠人记,没有自动提醒、没有漏斗分析
  2. 项目做到哪了? — 任务状态和工时手动填,跟合同金额/应收款脱节
  3. 钱收回来了吗? — 报价→合同→开票→收款割裂,没有串联
  4. 运维服务会不会忘? — 巡检计划写了没人催SLA 超时了才知道
  5. 税和利润算不清? — 收支分散在不同表里,月底做账要手动汇总

问题根因: 平台已有 trigger_events、settings、templates、cascade_from、visible_when、validation 六大能力,但两个插件完全没有使用。

改进目标: 纯插件层增强,三层递进:

  • Layer 1: 智能业务引擎 — 让系统主动驱动用户做事
  • Layer 2: 仪表盘重构 — 一个页面掌控全局
  • Layer 3: 专业输出 — 一键生成报价单/发票/合同 PDF

2. Layer 1: 智能业务引擎 — freelance 插件

2.1 Settings插件配置页

一次性配置公司信息和业务偏好,后续自动生效:

[settings]

  # ── 基本信息 ──
  [[settings.fields]]
  name = "company_name"
  display_name = "公司名称"
  field_type = "text"
  required = true
  group = "基本信息"

  [[settings.fields]]
  name = "currency_symbol"
  display_name = "货币符号"
  field_type = "text"
  default_value = "¥"
  group = "基本信息"

  # ── 财务 ──
  [[settings.fields]]
  name = "default_tax_rate"
  display_name = "默认税率(%)"
  field_type = "number"
  default_value = 6
  range = [0.0, 100.0]
  group = "财务"

  # ── 提醒 ──
  [[settings.fields]]
  name = "payment_reminder_days"
  display_name = "收款提前提醒(天)"
  field_type = "number"
  default_value = 3
  range = [1.0, 30.0]
  group = "提醒"

  [[settings.fields]]
  name = "notify_contract_expiring"
  display_name = "合同到期提醒"
  field_type = "boolean"
  default_value = true
  group = "提醒"

  [[settings.fields]]
  name = "notify_payment_overdue"
  display_name = "逾期收款提醒"
  field_type = "boolean"
  default_value = true
  group = "提醒"

  [[settings.fields]]
  name = "notify_opportunity_followup"
  display_name = "商机跟进提醒"
  field_type = "boolean"
  default_value = true
  group = "提醒"

2.2 Trigger Events自动事件驱动

关键操作时自动发通知,把"人找事"变"事找人"

[[trigger_events]]
name = "opportunity_stage_changed"
display_name = "商机阶段变更"
description = "商机阶段发生变化时通知,特别是成交或失败"
entity = "opportunity"
on = "update"

[[trigger_events]]
name = "contract_status_changed"
display_name = "合同状态变更"
description = "合同状态变化时检查到期预警"
entity = "contract"
on = "update"

[[trigger_events]]
name = "invoice_status_changed"
display_name = "发票状态变更"
description = "发票状态变化时检查逾期收款"
entity = "invoice"
on = "update"

[[trigger_events]]
name = "task_status_changed"
display_name = "任务状态变更"
description = "任务完成或取消时通知"
entity = "task"
on = "update"

[[trigger_events]]
name = "expense_created"
display_name = "新支出记录"
description = "记录新支出时通知"
entity = "expense"
on = "create"

2.3 Cascade智能联动下拉

选客户后自动过滤其关联数据。以下均为已有字段追加 cascade 属性,不是新增字段:

contract 实体 — 已有 opportunity_id 字段追加:

  cascade_from = "client_id"
  cascade_filter = "client_id"

contract 实体 — 已有 quote_id 字段追加:

  cascade_from = "client_id"
  cascade_filter = "client_id"

invoice 实体 — 已有 project_id 字段追加:

  cascade_from = "client_id"
  cascade_filter = "client_id"

invoice 实体 — 已有 contract_id 字段追加:

  cascade_from = "client_id"
  cascade_filter = "client_id"

time_entry 实体 — 已有 task_id 字段追加:

  cascade_from = "project_id"
  cascade_filter = "project_id"

2.4 Visible When条件显示

只在有意义时才显示字段。以下为已有字段追加 visible_when 属性

invoice 实体 — 已有 payment_date 字段追加:

  visible_when = "status == 'paid' || status == 'partial'"

contract 实体 — 已有 paid_amount 字段追加:

  visible_when = "status != 'drafting'"

task 实体 — 已有 actual_hours 字段追加:

  visible_when = "status != 'todo'"

quote 实体 — 已有 total_amount 字段追加:

  visible_when = "status != 'draft'"

2.5 Validation字段校验

已有字段追加 validation 属性,不是新增字段:

client 实体 — 已有 email 字段追加:

  validation = { pattern = "^[^@]+@[^@]+\\.[^@]+$", message = "请输入有效的邮箱地址" }

client 实体 — 已有 phone 字段追加:

  validation = { pattern = "^1[3-9]\\d{9}$", message = "请输入有效的手机号" }

3. Layer 2: 仪表盘重构 — freelance 插件

将占位符仪表盘升级为真正的指挥中心。通过 widgets 声明告诉平台该展示什么。

平台依赖: 仪表盘 widgets 需要平台层配合:

  1. manifest.rsPluginPageType::Dashboard 需要新增 widgets: Option<Vec<PluginWidget>> 字段
  2. 定义 PluginWidget 枚举stat_cards/action_list/funnel/card_list 类型)
  3. 更新 TOML 解析和验证逻辑
  4. 前端解析 widgets 声明并渲染对应组件

因此 P5/P6 不是纯 plugin.toml 改动,需要平台+前端联合实施。以下 widgets 声明作为设计参考,实施时需先完成平台侧支持。

[[ui.pages]]
type = "dashboard"
label = "工作台"
icon = "DashboardOutlined"

  # ── 财务概览卡片 ──
  [[ui.pages.widgets]]
  type = "stat_cards"
  label = "财务概览"
  cards = [
    { entity = "invoice", aggregate = "sum", field = "amount", filter = "type == 'payment' && status != 'overdue'", label = "本月收入", icon = "rise", color = "green" },
    { entity = "expense", aggregate = "sum", field = "amount", label = "本月支出", icon = "fall", color = "red" },
    { entity = "invoice", aggregate = "sum", field = "amount", filter = "status == 'overdue' || status == 'pending'", label = "应收总额", icon = "dollar", color = "orange" },
    { entity = "invoice", aggregate = "count", filter = "status == 'overdue'", label = "逾期笔数", icon = "warning", color = "red" }
  ]

  # ── 紧急待办 ──
  [[ui.pages.widgets]]
  type = "action_list"
  label = "紧急待办"
  max_items = 5
  queries = [
    { entity = "invoice", filter = "status == 'overdue'", label_field = "invoice_number", subtitle_field = "amount", action = "查看", icon = "warning" },
    { entity = "task", filter = "status != 'done' && status != 'cancelled'", sort = "due_date asc", label_field = "title", subtitle_field = "due_date", action = "处理", icon = "clock" },
    { entity = "contract", filter = "status == 'active'", sort = "end_date asc", label_field = "title", subtitle_field = "end_date", action = "续约", icon = "file-text" },
    { entity = "opportunity", filter = "next_follow_up <= today", label_field = "title", subtitle_field = "next_follow_up", action = "跟进", icon = "phone" }
  ]

  # ── 商机漏斗 ──
  [[ui.pages.widgets]]
  type = "funnel"
  label = "商机漏斗"
  entity = "opportunity"
  lane_field = "stage"
  value_field = "estimated_amount"
  lane_order = ["visit", "requirement", "quote", "negotiation", "won", "lost"]

  # ── 活跃项目卡片 ──
  [[ui.pages.widgets]]
  type = "card_list"
  label = "活跃项目"
  entity = "project"
  filter = "status == 'in_progress'"
  max_items = 4
  title_field = "name"
  subtitle_field = "contract_amount"
  tags = ["business_type", "status"]

依赖: 数据源来自平台已有的聚合 API/count/aggregate。Filter 表达式使用平台过滤 DSL==, !=, ||, &&, <=)。


4. Layer 3: 专业输出 — freelance 插件

一键生成专业 PDF替代手动排 Word。

模板引擎说明:

  • 语法基于 Handlebars{{field}}, {{#each relation}}...{{/each}}
  • 当前实体字段直接可用:{{amount}}, {{status}}
  • 关系字段解析:{{client.name}} 表示通过 client_id 引用的 client 实体的 name 字段,渲染器需自动解析
  • {{#each lines}} 用于一对多关系(如 quote → quote_line渲染器查询子实体并遍历
  • 平台需要实现 PDF 渲染管道TOML 模板 → Handlebars 渲染(注入数据)→ HTML → wkhtmltopdf/浏览器打印 → PDF

4.1 报价单模板

[[templates]]
name = "quote_pdf"
display_name = "报价单"
entity = "quote"
format = "pdf"
template_html = """
<html>
<head><style>
  body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; }
  h1 { text-align: center; border-bottom: 2px solid #333; padding-bottom: 10px; }
  table { width: 100%; border-collapse: collapse; margin: 20px 0; }
  th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
  th { background-color: #f5f5f5; }
  .total { text-align: right; font-size: 18px; font-weight: bold; }
  .footer { margin-top: 40px; color: #666; font-size: 12px; }
</style></head>
<body>
  <h1>报价单 {{quote_number}}</h1>
  <p>客户:{{client.name}} | 有效期至:{{valid_until}}</p>
  <table>
    <tr><th>项目</th><th>描述</th><th>数量</th><th>单价</th><th>金额</th></tr>
    {{#each lines}}
    <tr><td>{{item_name}}</td><td>{{description}}</td><td>{{quantity}}</td><td>{{unit_price}}</td><td>{{amount}}</td></tr>
    {{/each}}
  </table>
  <p class="total">小计:{{subtotal}} | 税率:{{tax_rate}}% | 总计:{{total_amount}}</p>
  <div class="footer">备注:{{notes}}</div>
</body>
</html>
"""

4.2 发票模板

[[templates]]
name = "invoice_pdf"
display_name = "发票"
entity = "invoice"
format = "pdf"
template_html = """
<html>
<head><style>
  body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; }
  h1 { text-align: center; color: #1890ff; border-bottom: 2px solid #1890ff; padding-bottom: 10px; }
  .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }
  .info-item { padding: 8px; background: #fafafa; }
  .amount { font-size: 24px; font-weight: bold; text-align: center; color: #f5222d; margin: 20px 0; }
  .status-badge { display: inline-block; padding: 4px 12px; border-radius: 4px; background: #f0f0f0; }
</style></head>
<body>
  <h1>发票 {{invoice_number}}</h1>
  <div class="info-grid">
    <div class="info-item">客户:{{client.name}}</div>
    <div class="info-item">类型:{{type}}</div>
    <div class="info-item">开票日期:{{issue_date}}</div>
    <div class="info-item">到期日:{{due_date}}</div>
  </div>
  <div class="amount">¥{{amount}}</div>
  <p>状态:<span class="status-badge">{{status}}</span></p>
  <p>备注:{{notes}}</p>
</body>
</html>
"""

4.3 合同模板

[[templates]]
name = "contract_pdf"
display_name = "合同"
entity = "contract"
format = "pdf"
template_html = """
<html>
<head><style>
  body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; }
  h1 { text-align: center; border-bottom: 3px double #333; padding-bottom: 10px; }
  .parties { margin: 20px 0; padding: 15px; background: #fafafa; border-left: 4px solid #1890ff; }
  .signature { margin-top: 60px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
  .sig-box { border-top: 1px solid #333; padding-top: 10px; text-align: center; }
</style></head>
<body>
  <h1>{{title}}</h1>
  <p>合同编号:{{contract_number}}</p>
  <div class="parties">
    <p>甲方:{{client.name}}</p>
    <p>合同金额:¥{{amount}} | 已付:¥{{paid_amount}}</p>
    <p>期限:{{start_date}} 至 {{end_date}}</p>
    <p>付款条款:{{payment_terms}}</p>
  </div>
  <p>备注:{{notes}}</p>
  <div class="signature">
    <div class="sig-box">甲方签章</div>
    <div class="sig-box">乙方签章</div>
  </div>
</body>
</html>
"""

5. itops 插件增强

5.1 Settings

[settings]

  [[settings.fields]]
  name = "default_sla_response"
  display_name = "默认SLA响应时间(小时)"
  field_type = "number"
  default_value = 8
  range = [1.0, 72.0]
  group = "SLA"

  [[settings.fields]]
  name = "default_sla_resolve"
  display_name = "默认SLA解决时间(小时)"
  field_type = "number"
  default_value = 48
  range = [1.0, 168.0]
  group = "SLA"

  [[settings.fields]]
  name = "notify_sla_breach"
  display_name = "SLA超标提醒"
  field_type = "boolean"
  default_value = true
  group = "提醒"

  [[settings.fields]]
  name = "notify_check_due"
  display_name = "巡检到期提醒"
  field_type = "boolean"
  default_value = true
  group = "提醒"

5.2 Trigger Events

[[trigger_events]]
name = "ticket_created"
display_name = "新工单"
description = "创建工单时开始SLA计时并通知"
entity = "ticket"
on = "create"

[[trigger_events]]
name = "ticket_status_changed"
display_name = "工单状态变更"
description = "工单状态变化时检查SLA是否达标"
entity = "ticket"
on = "update"

[[trigger_events]]
name = "contract_status_changed"
display_name = "维保合同状态变更"
description = "合同状态变化时检查到期预警"
entity = "service_contract"
on = "update"

[[trigger_events]]
name = "check_plan_updated"
display_name = "巡检计划更新"
description = "巡检计划更新时检查下次巡检日期"
entity = "check_plan"
on = "update"

5.3 Cascade

已有字段追加 cascade 属性,不是新增字段:

ticket 实体 — 已有 contract_id 字段追加:

  cascade_from = "client_id"
  cascade_filter = "client_id"

check_record 实体 — 已有 contract_id 字段追加:

  cascade_from = "plan_id"
  cascade_filter = "contract_id"

5.4 Visible When

已有字段追加 visible_when 属性

ticket 实体 — 已有 resolution 字段追加:

  visible_when = "status == 'resolved' || status == 'closed'"

ticket 实体 — 已有 responded_at 字段追加:

  visible_when = "status != 'open'"

ticket 实体 — 已有 resolved_at 字段追加:

  visible_when = "status == 'resolved' || status == 'closed'"

ticket 实体 — 已有 closed_at 字段追加:

  visible_when = "status == 'closed'"

check_record 实体 — 已有 issues_found 字段追加:

  visible_when = "result == 'abnormal'"

check_record 实体 — 已有 actions_taken 字段追加:

  visible_when = "result == 'abnormal'"

5.5 Validation

已有字段追加 validation 属性

service_contract 实体 — 已有 contract_number 字段追加:

  validation = { pattern = "^SC-\\d{4}-\\d{4}$", message = "格式SC-YYYY-NNNN" }

5.6 Dashboard

同 Layer 2 说明: widgets 需要平台层配合manifest.rs 扩展 + 前端渲染),非纯 plugin.toml 改动。此仪表盘页面插入到现有页面列表最前面,现有 4 个页面保持不变。

[[ui.pages]]
type = "dashboard"
label = "运维概览"
icon = "DashboardOutlined"

  [[ui.pages.widgets]]
  type = "stat_cards"
  label = "运维概览"
  cards = [
    { entity = "service_contract", aggregate = "count", filter = "status == 'active'", label = "活跃合同", icon = "file-text", color = "blue" },
    { entity = "ticket", aggregate = "count", filter = "status == 'open' || status == 'in_progress'", label = "待处理工单", icon = "tool", color = "orange" },
    { entity = "ticket", aggregate = "count", filter = "status == 'resolved'", label = "已解决工单", icon = "check-circle", color = "green" },
    { entity = "check_plan", aggregate = "count", filter = "status == 'active'", label = "活跃巡检", icon = "schedule", color = "blue" }
  ]

  [[ui.pages.widgets]]
  type = "action_list"
  label = "紧急待办"
  max_items = 5
  queries = [
    { entity = "ticket", filter = "status == 'open'", sort = "priority asc", label_field = "title", subtitle_field = "type", action = "处理", icon = "warning" },
    { entity = "service_contract", filter = "status == 'active'", sort = "end_date asc", label_field = "name", subtitle_field = "end_date", action = "续约", icon = "file-text" },
    { entity = "check_plan", filter = "status == 'active'", sort = "next_check_date asc", label_field = "name", subtitle_field = "next_check_date", action = "巡检", icon = "schedule" }
  ]

5.7 Template维保合同 PDF

[[templates]]
name = "service_contract_pdf"
display_name = "维保合同"
entity = "service_contract"
format = "pdf"
template_html = """
<html>
<head><style>
  body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; }
  h1 { text-align: center; border-bottom: 3px double #1890ff; padding-bottom: 10px; color: #1890ff; }
  .sla-box { margin: 20px 0; padding: 15px; background: #e6f7ff; border: 1px solid #91d5ff; border-radius: 4px; }
  .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }
  .info-item { padding: 8px; background: #fafafa; }
  .signature { margin-top: 60px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
  .sig-box { border-top: 1px solid #333; padding-top: 10px; text-align: center; }
</style></head>
<body>
  <h1>{{name}}</h1>
  <p>合同编号:{{contract_number}}</p>
  <div class="info-grid">
    <div class="info-item">客户:{{client.name}}</div>
    <div class="info-item">合同金额:¥{{amount}}</div>
    <div class="info-item">期限:{{start_date}} 至 {{end_date}}</div>
    <div class="info-item">状态:{{status}}</div>
  </div>
  <div class="sla-box">
    <strong>SLA 承诺:</strong>响应 {{sla_response_hours}} 小时内 / 解决 {{sla_resolve_hours}} 小时内
  </div>
  <p>服务范围:{{service_scope}}</p>
  <p>付款条款:{{payment_terms}}</p>
  <p>备注:{{notes}}</p>
  <div class="signature">
    <div class="sig-box">甲方签章</div>
    <div class="sig-box">乙方签章</div>
  </div>
</body>
</html>
"""

6. 改进汇总

层次 能力 freelance itops
Layer 1 settings 7 个配置项(公司名/税率/提醒偏好) 4 个配置项SLA默认值/提醒偏好)
Layer 1 trigger_events 5 个事件(商机/合同/发票/任务/支出) 4 个事件(工单/合同/巡检)
Layer 1 cascade 4 处联动(合同/发票/工时表单) 2 处联动(工单/巡检记录)
Layer 1 visible_when 4 个条件字段 6 个条件字段
Layer 1 validation 2 个校验(邮箱/手机) 1 个校验(合同编号格式)
Layer 2 dashboard widgets 财务卡片+紧急待办+商机漏斗+项目卡片 运维卡片+紧急待办
Layer 3 templates 3 个 PDF报价单/发票/合同) 1 个 PDF维保合同

总计: 2 个插件 × 3 层增强,从「数据录入」升级为「赚钱工具」。


7. 实施优先级

P1: freelance Layer 1settings + trigger_events + cascade + visible_when + validation
P2: itops Layer 1settings + trigger_events + cascade + visible_when + validation
P3: freelance Layer 33 个 PDF 模板)
P4: itops Layer 3维保合同 PDF 模板)
P5: freelance Layer 2仪表盘 widgets
P6: itops Layer 2仪表盘 widgets

P1-P4 是纯 plugin.toml 改动(给已有字段追加 cascade/visible_when/validation 属性,以及新增 settings/trigger_events/templates 段落可立即实施。P5-P6 的仪表盘 widgets 需要平台层配合:扩展 manifest.rsPluginPageType::Dashboard 支持 widgets 字段 + 前端渲染组件。