feat(plugin,freelance,itops,web): P5-P6 dashboard widgets 平台扩展 + 仪表盘声明
P5 平台扩展: - manifest.rs: Dashboard 变体新增 widgets 字段 - manifest.rs: 定义 PluginWidget/StatCard/ActionQuery 类型 - 前端: 扩展 DashboardWidget 类型支持 stat_cards/action_list/funnel/card_list - 前端: 新增 4 个 widget 渲染器 (StatCardsWidget/ActionListWidget/FunnelStageWidget/CardListWidget) - 前端: PluginDashboardPage widget 数据加载支持新类型 P6 仪表盘 widgets: - freelance: 工作台仪表盘 4 个 widgets (财务概览/紧急待办/商机漏斗/活跃项目) - itops: 新增运维概览仪表盘 2 个 widgets (运维概览/紧急待办)
This commit is contained in:
@@ -284,6 +284,8 @@ pub enum PluginPageType {
|
||||
label: String,
|
||||
#[serde(default)]
|
||||
icon: Option<String>,
|
||||
#[serde(default)]
|
||||
widgets: Vec<PluginWidget>,
|
||||
},
|
||||
#[serde(rename = "kanban")]
|
||||
Kanban {
|
||||
@@ -304,6 +306,80 @@ pub enum PluginPageType {
|
||||
},
|
||||
}
|
||||
|
||||
/// Dashboard Widget 类型
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum PluginWidget {
|
||||
#[serde(rename = "stat_cards")]
|
||||
StatCards {
|
||||
label: String,
|
||||
cards: Vec<StatCard>,
|
||||
},
|
||||
#[serde(rename = "action_list")]
|
||||
ActionList {
|
||||
label: String,
|
||||
#[serde(default)]
|
||||
max_items: Option<u32>,
|
||||
queries: Vec<ActionQuery>,
|
||||
},
|
||||
#[serde(rename = "funnel")]
|
||||
Funnel {
|
||||
label: String,
|
||||
entity: String,
|
||||
lane_field: String,
|
||||
#[serde(default)]
|
||||
value_field: Option<String>,
|
||||
lane_order: Vec<String>,
|
||||
},
|
||||
#[serde(rename = "card_list")]
|
||||
CardList {
|
||||
label: String,
|
||||
entity: String,
|
||||
#[serde(default)]
|
||||
filter: Option<String>,
|
||||
#[serde(default)]
|
||||
max_items: Option<u32>,
|
||||
title_field: String,
|
||||
#[serde(default)]
|
||||
subtitle_field: Option<String>,
|
||||
#[serde(default)]
|
||||
tags: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// 统计卡片
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct StatCard {
|
||||
pub entity: String,
|
||||
#[serde(default)]
|
||||
pub aggregate: Option<String>,
|
||||
#[serde(default)]
|
||||
pub field: Option<String>,
|
||||
#[serde(default)]
|
||||
pub filter: Option<String>,
|
||||
pub label: String,
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
#[serde(default)]
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
/// 待办行动查询
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ActionQuery {
|
||||
pub entity: String,
|
||||
#[serde(default)]
|
||||
pub filter: Option<String>,
|
||||
#[serde(default)]
|
||||
pub sort: Option<String>,
|
||||
pub label_field: String,
|
||||
#[serde(default)]
|
||||
pub subtitle_field: Option<String>,
|
||||
pub action: String,
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
/// 插件页面区段(用于 detail 页面类型)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -1553,4 +1629,153 @@ name = "管理发票"
|
||||
assert_eq!(entities[0].importable, Some(true));
|
||||
assert_eq!(entities[0].exportable, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_dashboard_with_widgets() {
|
||||
let toml = r##"
|
||||
[metadata]
|
||||
id = "test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
|
||||
[schema]
|
||||
[[schema.entities]]
|
||||
name = "invoice"
|
||||
display_name = "发票"
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "status"
|
||||
field_type = "string"
|
||||
display_name = "状态"
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "amount"
|
||||
field_type = "decimal"
|
||||
display_name = "金额"
|
||||
|
||||
[ui]
|
||||
[[ui.pages]]
|
||||
type = "dashboard"
|
||||
label = "工作台"
|
||||
icon = "DashboardOutlined"
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "stat_cards"
|
||||
label = "财务概览"
|
||||
|
||||
[[ui.pages.widgets.cards]]
|
||||
entity = "invoice"
|
||||
aggregate = "count"
|
||||
label = "总发票"
|
||||
icon = "FileTextOutlined"
|
||||
color = "#1890ff"
|
||||
|
||||
[[ui.pages.widgets.cards]]
|
||||
entity = "invoice"
|
||||
aggregate = "sum"
|
||||
field = "amount"
|
||||
filter = "status == 'pending'"
|
||||
label = "待收金额"
|
||||
icon = "DollarOutlined"
|
||||
color = "#faad14"
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "action_list"
|
||||
label = "紧急待办"
|
||||
max_items = 5
|
||||
|
||||
[[ui.pages.widgets.queries]]
|
||||
entity = "invoice"
|
||||
filter = "status == 'overdue'"
|
||||
sort = "due_date asc"
|
||||
label_field = "invoice_number"
|
||||
subtitle_field = "amount"
|
||||
action = "open_invoice"
|
||||
icon = "warning"
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "funnel"
|
||||
label = "商机漏斗"
|
||||
entity = "invoice"
|
||||
lane_field = "status"
|
||||
value_field = "amount"
|
||||
lane_order = ["pending", "issued", "paid"]
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "card_list"
|
||||
label = "活跃项目"
|
||||
entity = "invoice"
|
||||
filter = "status == 'active'"
|
||||
max_items = 10
|
||||
title_field = "invoice_number"
|
||||
subtitle_field = "amount"
|
||||
tags = ["status"]
|
||||
"##;
|
||||
let manifest = parse_manifest(toml).unwrap();
|
||||
let ui = manifest.ui.unwrap();
|
||||
assert_eq!(ui.pages.len(), 1);
|
||||
match &ui.pages[0] {
|
||||
PluginPageType::Dashboard {
|
||||
label, icon, widgets,
|
||||
} => {
|
||||
assert_eq!(label, "工作台");
|
||||
assert_eq!(icon.as_deref(), Some("DashboardOutlined"));
|
||||
assert_eq!(widgets.len(), 4);
|
||||
|
||||
// stat_cards
|
||||
match &widgets[0] {
|
||||
PluginWidget::StatCards { label, cards } => {
|
||||
assert_eq!(label, "财务概览");
|
||||
assert_eq!(cards.len(), 2);
|
||||
assert_eq!(cards[0].entity, "invoice");
|
||||
assert_eq!(cards[0].aggregate.as_deref(), Some("count"));
|
||||
assert_eq!(cards[1].aggregate.as_deref(), Some("sum"));
|
||||
assert_eq!(cards[1].filter.as_deref(), Some("status == 'pending'"));
|
||||
}
|
||||
_ => panic!("Expected StatCards"),
|
||||
}
|
||||
|
||||
// action_list
|
||||
match &widgets[1] {
|
||||
PluginWidget::ActionList {
|
||||
label, max_items, queries,
|
||||
} => {
|
||||
assert_eq!(label, "紧急待办");
|
||||
assert_eq!(*max_items, Some(5));
|
||||
assert_eq!(queries.len(), 1);
|
||||
assert_eq!(queries[0].entity, "invoice");
|
||||
assert_eq!(queries[0].action, "open_invoice");
|
||||
}
|
||||
_ => panic!("Expected ActionList"),
|
||||
}
|
||||
|
||||
// funnel
|
||||
match &widgets[2] {
|
||||
PluginWidget::Funnel {
|
||||
label, entity, lane_field, value_field, lane_order,
|
||||
} => {
|
||||
assert_eq!(label, "商机漏斗");
|
||||
assert_eq!(entity, "invoice");
|
||||
assert_eq!(lane_field, "status");
|
||||
assert_eq!(value_field.as_deref(), Some("amount"));
|
||||
assert_eq!(lane_order, &["pending", "issued", "paid"]);
|
||||
}
|
||||
_ => panic!("Expected Funnel"),
|
||||
}
|
||||
|
||||
// card_list
|
||||
match &widgets[3] {
|
||||
PluginWidget::CardList {
|
||||
label, entity, title_field, ..
|
||||
} => {
|
||||
assert_eq!(label, "活跃项目");
|
||||
assert_eq!(entity, "invoice");
|
||||
assert_eq!(title_field, "invoice_number");
|
||||
}
|
||||
_ => panic!("Expected CardList"),
|
||||
}
|
||||
}
|
||||
_ => panic!("Expected Dashboard page type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user