33import { definePage } from '@objectstack/spec/ui' ;
44
55/**
6- * Operations Command Center (大屏) — a dense, full-bleed "big screen" built as a
7- * pure SDUI micro-page that FOLLOWS the console theme (light ↔ dark).
6+ * Operations Command Center (大屏) — a full-bleed SDUI "big screen" that follows
7+ * the console theme (light ↔ dark). It exercises the data-screen customization
8+ * primitives added to objectui (#1922 + follow-up):
9+ * • object-metric `variant:'bare'` → live KPIs as big tinted numbers (no card
10+ * chrome) — data-bound, not hand-typed.
11+ * • object-chart `colors` → one cohesive brand palette across every chart
12+ * (replaces the old `--chart-1..5` override hack).
13+ * • object-chart `chartType:'donut'` → first-class (was an untyped string).
14+ * • full-bleed page + container-scaled donuts + gradient bars/areas.
815 *
9- * Density: object-chart's ChartContainer is `h-[350px]` by default, but it
10- * honours a consumer className (→ AdvancedChartImpl → ChartContainer), so each
11- * chart node carries a `responsiveStyles` height that shrinks it toward the
12- * ~280px floor. Compact title + KPI strip + tight 2-col rows then fit the whole
13- * board — KPIs, five charts and the work queue — without a long scroll.
16+ * Composition: centred title → full-width KPI hero strip (6 live metrics) →
17+ * two equal chart rows (throughput trend spans 2) → work queue on its own
18+ * full-width row (height never knocks a chart row out of alignment).
1419 *
15- * Theme-adaptive: every colour is a theme token (`hsl(var(--card))` panels,
16- * `hsl(var(--foreground))` text, `hsl(var(--border))`). The root overrides
17- * `--chart-1..5` with one cohesive mid-lightness ramp (reads on light AND dark);
18- * KPI numbers reuse it.
19- *
20- * Layout note: object-chart must sit in a `display: block` panel, and the root
21- * column needs `align-items: stretch` or children shrink to content width.
20+ * Theme-adaptive: panels/text/hairlines are theme tokens (`hsl(var(--card))` …).
21+ * Layout note: object-chart must sit in a `display:block` panel (a flex child
22+ * collapses to width:0 and recharts won't draw).
2223 */
2324
24- const CHART_RAMP = {
25- '--chart-1' : '192 86% 46%' ,
26- '--chart-2' : '214 84% 56%' ,
27- '--chart-3' : '256 72% 62%' ,
28- '--chart-4' : '168 76% 42%' ,
29- '--chart-5' : '322 72% 56%' ,
30- } ;
25+ // One cohesive brand palette, mid-lightness so it reads on light AND dark.
26+ const PALETTE = [ 'hsl(192 86% 46%)' , 'hsl(214 84% 56%)' , 'hsl(256 72% 62%)' , 'hsl(168 76% 42%)' , 'hsl(322 72% 56%)' ] ;
27+ const A = { c1 : PALETTE [ 0 ] , c2 : PALETTE [ 1 ] , c3 : PALETTE [ 2 ] , c4 : PALETTE [ 3 ] , c5 : PALETTE [ 4 ] } ;
3128
3229function head ( id : string , title : string , accent : string , badge ?: string ) : any {
3330 return {
3431 id : id + '_h' , type : 'flex' ,
35- responsiveStyles : { large : { display : 'flex' , alignItems : 'center' , gap : '8px ' , marginBottom : '8px ' , paddingBottom : '7px ' , borderBottom : '1px solid hsl(var(--border))' } } ,
32+ responsiveStyles : { large : { display : 'flex' , alignItems : 'center' , gap : '9px ' , marginBottom : '12px ' , paddingBottom : '10px ' , borderBottom : '1px solid hsl(var(--border))' } } ,
3633 properties : {
3734 children : [
38- { id : id + '_bar' , type : 'flex' , responsiveStyles : { large : { width : '4px' , height : '13px ' , borderRadius : '2px' , background : accent , boxShadow : '0 0 9px ' + accent } } , properties : { children : [ ] } } ,
39- { id : id + '_t' , type : 'element:text' , responsiveStyles : { large : { fontSize : '13px ' , fontWeight : '700' , letterSpacing : '0.03em' , color : 'hsl(var(--foreground))' } } , properties : { content : title } } ,
35+ { id : id + '_bar' , type : 'flex' , responsiveStyles : { large : { width : '4px' , height : '15px ' , borderRadius : '2px' , background : accent , boxShadow : '0 0 10px ' + accent } } , properties : { children : [ ] } } ,
36+ { id : id + '_t' , type : 'element:text' , responsiveStyles : { large : { fontSize : '14px ' , fontWeight : '700' , letterSpacing : '0.03em' , color : 'hsl(var(--foreground))' } } , properties : { content : title } } ,
4037 ...( badge
4138 ? [
4239 { id : id + '_sp' , type : 'flex' , responsiveStyles : { large : { flex : '1 1 auto' } } , properties : { children : [ ] } } ,
43- { id : id + '_b' , type : 'element:text' , responsiveStyles : { large : { fontSize : '10px ' , fontWeight : '700' , color : 'hsl(var(--chart-2))' , background : 'hsl(var(--chart-2) / 0.12)' , border : '1px solid hsl(var(--chart-2) / 0.4)' , borderRadius : '999px' , padding : '2px 9px ' } } , properties : { content : badge } } ,
40+ { id : id + '_b' , type : 'element:text' , responsiveStyles : { large : { fontSize : '11px ' , fontWeight : '700' , color : A . c2 , background : 'hsl(214 84% 56% / 0.12)' , border : '1px solid hsl(214 84% 56% / 0.4)' , borderRadius : '999px' , padding : '3px 11px ' } } , properties : { content : badge } } ,
4441 ]
4542 : [ ] ) ,
4643 ] ,
@@ -53,20 +50,20 @@ function panel(o: { id: string; title?: string; accent: string; badge?: string;
5350 id : o . id , type : 'flex' ,
5451 responsiveStyles : {
5552 large : {
56- display : 'block' , minWidth : '0' , minHeight : o . minHeight ?? '0px ' ,
53+ display : 'block' , minWidth : '0' , minHeight : o . minHeight ?? '240px ' ,
5754 ...( o . span ? { gridColumn : o . span } : { } ) ,
58- padding : o . pad ?? '12px 14px 12px ' , borderRadius : '14px ' ,
55+ padding : o . pad ?? '15px 17px 17px ' , borderRadius : '16px ' ,
5956 background : 'hsl(var(--card))' ,
6057 border : '1px solid hsl(var(--border))' ,
61- boxShadow : '0 14px 34px -24px rgba(2,6,23,0.45), inset 0 1px 0 hsl(var(--foreground) / 0.04)' ,
58+ boxShadow : '0 16px 40px -24px rgba(2,6,23,0.45), inset 0 1px 0 hsl(var(--foreground) / 0.04)' ,
6259 } ,
63- small : { padding : '12px' } ,
60+ small : { padding : '12px' , minHeight : '200px' } ,
6461 } ,
6562 properties : { children : [ ...( o . title ? [ head ( o . id , o . title , o . accent , o . badge ) ] : [ ] ) , o . child ] } ,
6663 } ;
6764}
6865
69- function band ( id : string , cols : number , children : any [ ] , gap = '12px ' ) : any {
66+ function band ( id : string , cols : number , children : any [ ] , gap = '16px ' ) : any {
7067 return {
7168 id, type : 'flex' ,
7269 responsiveStyles : {
@@ -78,26 +75,21 @@ function band(id: string, cols: number, children: any[], gap = '12px'): any {
7875 } ;
7976}
8077
81- function stat ( id : string , value : string , label : string , chartVar : string ) : any {
78+ /** A LIVE KPI — object-metric in the new `bare` variant (big tinted number + label). */
79+ function kpi ( id : string , object : string , label : string , colorVariant : string , aggregate : any , filter ?: any , format ?: string ) : any {
8280 return {
83- id, type : 'flex' ,
84- responsiveStyles : { large : { display : 'flex' , flexDirection : 'column' , gap : '1px' , padding : '2px 4px' } } ,
85- properties : {
86- children : [
87- { id : id + '_v' , type : 'element:text' , responsiveStyles : { large : { fontSize : '30px' , fontWeight : '800' , lineHeight : '1.05' , color : 'hsl(var(' + chartVar + '))' , fontVariantNumeric : 'tabular-nums' } } , properties : { content : value } } ,
88- { id : id + '_l' , type : 'element:text' , responsiveStyles : { large : { fontSize : '11px' , fontWeight : '500' , letterSpacing : '0.04em' , color : 'hsl(var(--muted-foreground))' } } , properties : { content : label } } ,
89- ] ,
90- } ,
81+ id, type : 'object-metric' ,
82+ responsiveStyles : { large : { minWidth : '0' } } ,
83+ properties : { objectName : object , label, colorVariant, variant : 'bare' , aggregate, ...( filter ? { filter } : { } ) , ...( format ? { format } : { } ) } ,
9184 } ;
9285}
9386
94- // Each chart node carries a height → shrinks the ChartContainer toward its
95- // ~280px floor (denser than the default h-[350px]).
87+ /** A dataset-bound chart with the shared brand palette. */
9688function chart ( id : string , chartType : string , dataset : string , dimensions : string [ ] , values : string [ ] ) : any {
97- return { id, type : 'object-chart' , responsiveStyles : { large : { width : '100%' , minWidth : '0' , height : '200px' } } , properties : { dataset, dimensions, values, chartType } } ;
89+ return { id, type : 'object-chart' , responsiveStyles : { large : { width : '100%' , minWidth : '0' } } , properties : { dataset, dimensions, values, chartType, colors : PALETTE } } ;
9890}
9991
100- const A = { c1 : 'hsl(var(--chart-1))' , c2 : 'hsl(var(--chart-2))' , c3 : 'hsl(var(--chart-3))' , c4 : 'hsl(var(--chart-4))' , c5 : 'hsl(var(--chart-5))' } ;
92+ const CHART_H = '376px' ;
10193
10294export const CommandCenterPage = definePage ( {
10395 name : 'showcase_command_center' ,
@@ -116,59 +108,60 @@ export const CommandCenterPage = definePage({
116108 type : 'flex' ,
117109 responsiveStyles : {
118110 large : {
119- ...CHART_RAMP ,
120- minHeight : '100%' , width : '100%' , display : 'flex' , flexDirection : 'column' , alignItems : 'stretch' , gap : '10px' ,
121- padding : '10px 20px 18px' ,
111+ minHeight : '100%' , display : 'flex' , flexDirection : 'column' , gap : '16px' ,
112+ padding : '22px 26px 32px' ,
122113 background :
123- 'radial-gradient(1200px 520px at 50% -16%, hsl(var(--chart-1) / 0.10) 0%, transparent 60%), ' +
114+ 'radial-gradient(1200px 540px at 50% -14%, hsl(192 86% 46% / 0.10) 0%, transparent 60%), ' +
115+ 'radial-gradient(900px 460px at 100% 0%, hsl(256 72% 62% / 0.08) 0%, transparent 55%), ' +
124116 'hsl(var(--background))' ,
125117 color : 'hsl(var(--foreground))' ,
126118 } ,
127- small : { padding : '12px' , gap : '10px ' } ,
119+ small : { padding : '14px 12px 24px ' , gap : '12px ' } ,
128120 } ,
129121 properties : {
130122 children : [
131- // ── Title (compact) ─────────────────────────────────────────
123+ // ── Title ─────────── ─────────────────────────────────────────
132124 {
133125 id : 'cc_titlebar' , type : 'flex' ,
134- responsiveStyles : { large : { display : 'flex' , flexDirection : 'column' , alignItems : 'center' , gap : '2px ' , padding : '0 ' } } ,
126+ responsiveStyles : { large : { display : 'flex' , flexDirection : 'column' , alignItems : 'center' , gap : '5px ' , padding : '2px 0 2px ' } } ,
135127 properties : {
136128 children : [
137- { id : 'cc_title' , type : 'element:text' , responsiveStyles : { large : { fontSize : '23px' , fontWeight : '800' , letterSpacing : '0.36em' , color : 'hsl(var(--foreground))' , textShadow : '0 0 22px hsl(var(--chart-1) / 0.45)' } , small : { fontSize : '17px' , letterSpacing : '0.14em' } } , properties : { content : '交 付 运 营 数 据 大 屏' } } ,
138- { id : 'cc_subtitle' , type : 'element:text' , responsiveStyles : { large : { fontSize : '10px' , fontWeight : '600' , letterSpacing : '0.34em' , color : 'hsl(var(--muted-foreground))' } } , properties : { content : 'DELIVERY OPERATIONS · COMMAND CENTER' } } ,
129+ { id : 'cc_title' , type : 'element:text' , responsiveStyles : { large : { fontSize : '27px' , fontWeight : '800' , letterSpacing : '0.4em' , color : 'hsl(var(--foreground))' , textShadow : '0 0 26px hsl(192 86% 46% / 0.45)' } , small : { fontSize : '18px' , letterSpacing : '0.16em' } } , properties : { content : '交 付 运 营 数 据 大 屏' } } ,
130+ { id : 'cc_subtitle' , type : 'element:text' , responsiveStyles : { large : { fontSize : '11px' , fontWeight : '600' , letterSpacing : '0.36em' , color : 'hsl(var(--muted-foreground))' } } , properties : { content : 'DELIVERY OPERATIONS · COMMAND CENTER' } } ,
131+ { id : 'cc_rule' , type : 'flex' , responsiveStyles : { large : { width : 'min(640px, 60%)' , height : '1px' , marginTop : '4px' , background : 'linear-gradient(90deg, transparent, hsl(192 86% 46% / 0.55), transparent)' } } , properties : { children : [ ] } } ,
139132 ] ,
140133 } ,
141134 } ,
142135
143- // ── KPI hero strip — 6 metrics across the full width ─────────
136+ // ── KPI hero strip — 6 LIVE metrics (bare variant) ── ─────────
144137 panel ( {
145- id : 'cc_kpi' , accent : A . c3 , pad : '10px 16px' ,
138+ id : 'cc_kpi' , title : '核心指标 · Key Metrics' , accent : A . c3 , minHeight : '0px' , pad : '14px 18px 16px' ,
146139 child : band ( 'cc_kpi_grid' , 6 , [
147- stat ( 'cc_k1' , '5 ' , '项目 Projects ' , '--chart-1' ) ,
148- stat ( 'cc_k2' , '10 ' , '任务 Tasks ' , '--chart-2' ) ,
149- stat ( 'cc_k3' , '13 ' , '客户 Accounts ' , '--chart-3' ) ,
150- stat ( 'cc_k4' , '8 ' , '待办 Open ' , '--chart-4' ) ,
151- stat ( 'cc_k5' , '2 ' , '复审 Review ' , '--chart-5' ) ,
152- stat ( 'cc_k6' , '1.09M ' , '预算 Budget' , '--chart-1 ' ) ,
153- ] , '8px ' ) ,
140+ kpi ( 'cc_k1' , 'showcase_project ' , '活跃项目 Active ' , 'blue' , { field : 'id' , function : 'count' } , { status : 'active' } ) ,
141+ kpi ( 'cc_k2' , 'showcase_task ' , '待办任务 Open ' , 'teal' , { field : 'id' , function : 'count' } , { status : { $ne : 'done' } } ) ,
142+ kpi ( 'cc_k3' , 'showcase_task ' , '待复审 Review ' , 'purple' , { field : 'id' , function : 'count' } , { status : 'in_review' } ) ,
143+ kpi ( 'cc_k4' , 'showcase_project ' , '风险项目 At-Risk ' , 'danger' , { field : 'id' , function : 'count' } , { health : 'red' } ) ,
144+ kpi ( 'cc_k5' , 'showcase_account ' , '客户 Accounts ' , 'orange' , { field : 'id' , function : 'count' } ) ,
145+ kpi ( 'cc_k6' , 'showcase_project ' , '总预算 Budget' , 'success' , { field : 'budget' , function : 'sum' } , undefined , '0.0a ') ,
146+ ] , '10px ' ) ,
154147 } ) ,
155148
156- // ── Row 1 — trend (wide) + status ───────────────────────────
157- band ( 'cc_r1' , 3 , [
158- panel ( { id : 'cc_throughput' , title : '任务吞吐趋势 (月)' , accent : A . c2 , span : 'span 2' , child : chart ( 'cc_thr_c' , 'area' , 'showcase_task_metrics' , [ 'created_at' ] , [ 'task_count' ] ) } ) ,
159- panel ( { id : 'cc_status' , title : '任务状态分布' , accent : A . c1 , child : chart ( 'cc_status_c' , 'bar' , 'showcase_task_metrics' , [ 'status' ] , [ 'task_count' ] ) } ) ,
149+ // ── Row A — three equal chart panels ─────────────────────────
150+ band ( 'cc_rowA' , 3 , [
151+ panel ( { id : 'cc_status' , title : '任务状态分布' , accent : A . c1 , minHeight : CHART_H , child : chart ( 'cc_status_c' , 'bar' , 'showcase_task_metrics' , [ 'status' ] , [ 'task_count' ] ) } ) ,
152+ panel ( { id : 'cc_health' , title : '项目健康度' , accent : A . c4 , minHeight : CHART_H , child : chart ( 'cc_health_c' , 'donut' , 'showcase_project_metrics' , [ 'health' ] , [ 'project_count' ] ) } ) ,
153+ panel ( { id : 'cc_priority' , title : '优先级分布' , accent : A . c5 , minHeight : CHART_H , child : chart ( 'cc_pri_c' , 'bar' , 'showcase_task_metrics' , [ 'priority' ] , [ 'task_count' ] ) } ) ,
160154 ] ) ,
161155
162- // ── Row 2 — three charts ────────────────────────────────────
163- band ( 'cc_r2' , 3 , [
164- panel ( { id : 'cc_priority' , title : '优先级分布' , accent : A . c5 , child : chart ( 'cc_pri_c' , 'bar' , 'showcase_task_metrics' , [ 'priority' ] , [ 'task_count' ] ) } ) ,
165- panel ( { id : 'cc_budget' , title : '预算 vs 支出 (按客户)' , accent : A . c4 , child : chart ( 'cc_bud_c' , 'bar' , 'showcase_project_metrics' , [ 'account' ] , [ 'budget_sum' , 'spent_sum' ] ) } ) ,
166- panel ( { id : 'cc_health' , title : '项目健康度' , accent : A . c4 , child : chart ( 'cc_health_c' , 'donut' , 'showcase_project_metrics' , [ 'health' ] , [ 'project_count' ] ) } ) ,
156+ // ── Row B — wide trend (span 2) + spend ──────────────────────
157+ band ( 'cc_rowB' , 3 , [
158+ panel ( { id : 'cc_throughput' , title : '任务吞吐趋势 (月)' , accent : A . c2 , span : 'span 2' , minHeight : CHART_H , child : chart ( 'cc_thr_c' , 'area' , 'showcase_task_metrics' , [ 'created_at' ] , [ 'task_count' ] ) } ) ,
159+ panel ( { id : 'cc_budget' , title : '预算 vs 支出 (按客户)' , accent : A . c4 , minHeight : CHART_H , child : chart ( 'cc_bud_c' , 'bar' , 'showcase_project_metrics' , [ 'account' ] , [ 'budget_sum' , 'spent_sum' ] ) } ) ,
167160 ] ) ,
168161
169- // ── Row 3 — work queue, full width, compact ───────── ────────
162+ // ── Work queue — its own full- width row at the bottom ────────
170163 panel ( {
171- id : 'cc_queue' , title : '待审核列表 · Work Queue' , accent : A . c1 , badge : '审批中' , pad : '12px 14px 8px ' ,
164+ id : 'cc_queue' , title : '待审核列表 · Work Queue' , accent : A . c1 , badge : '审批中' , minHeight : '0px ' ,
172165 child : { id : 'cc_queue_g' , type : 'object-grid' , responsiveStyles : { large : { minWidth : '0' , display : 'block' } } , properties : { objectName : 'showcase_task' , columns : [ 'title' , 'project' , 'status' , 'priority' , 'due_date' ] } } ,
173166 } ) ,
174167 ] ,
0 commit comments