@finebi/finebi-cli 1.6.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/agent-installation-guide.md +70 -0
- package/package.json +2 -1
- package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
- package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
- package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +160 -157
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +55 -55
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +95 -94
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -80
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/gitignore.template +6 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -63
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -40
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -3
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -58
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -235
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -20
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -70
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -189
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
- package/src/app.js +160 -157
- package/src/config.js +27 -27
- package/src/help.js +55 -55
- package/src/install-skill.js +89 -89
- package/src/profiles.js +158 -158
- package/src/skill-files.js +95 -94
- package/templates/web-app/docs/acceptance.md +80 -80
- package/templates/web-app/examples/cockpit.config.json +22 -22
- package/templates/web-app/examples/retail.config.json +185 -185
- package/templates/web-app/gitignore.template +6 -0
- package/templates/web-app/index.html +2 -2
- package/templates/web-app/public/app-config.json +11 -11
- package/templates/web-app/src/App.tsx +63 -63
- package/templates/web-app/src/cockpit.css +3 -3
- package/templates/web-app/src/components/Chart.tsx +40 -40
- package/templates/web-app/src/components/Insights.tsx +17 -17
- package/templates/web-app/src/components/MonthPicker.tsx +32 -32
- package/templates/web-app/src/components/OptionFilter.tsx +7 -7
- package/templates/web-app/src/components/RangeFilter.tsx +9 -9
- package/templates/web-app/src/components/Widget.tsx +40 -40
- package/templates/web-app/src/components/calendar-locales.ts +3 -3
- package/templates/web-app/src/components/calendar.ts +14 -14
- package/templates/web-app/src/components/charts/Bar.tsx +6 -6
- package/templates/web-app/src/components/charts/Line.tsx +6 -6
- package/templates/web-app/src/components/charts/Pie.tsx +6 -6
- package/templates/web-app/src/components/month-picker.css +1 -1
- package/templates/web-app/src/finebi/config.d.ts +3 -3
- package/templates/web-app/src/finebi/config.js +58 -58
- package/templates/web-app/src/finebi/date.d.ts +1 -1
- package/templates/web-app/src/finebi/date.js +5 -5
- package/templates/web-app/src/finebi/filters.d.ts +4 -4
- package/templates/web-app/src/finebi/filters.js +106 -106
- package/templates/web-app/src/finebi/normalize.ts +235 -235
- package/templates/web-app/src/finebi/types.ts +20 -20
- package/templates/web-app/src/main.tsx +5 -5
- package/templates/web-app/src/retail.css +12 -12
- package/templates/web-app/src/services/presentation.ts +36 -36
- package/templates/web-app/src/services/table-data.ts +47 -47
- package/templates/web-app/src/services/view-data.ts +18 -18
- package/templates/web-app/src/services/widget-queries.ts +18 -18
- package/templates/web-app/tests/browser/fixtures.mjs +28 -28
- package/templates/web-app/tests/browser/index.html +16 -16
- package/templates/web-app/tests/browser/main.tsx +149 -149
- package/templates/web-app/tests/browser/test-page.css +4 -4
- package/templates/web-app/tests/browser/vite.config.mjs +13 -13
- package/templates/web-app/tests/bundle.test.mjs +19 -19
- package/templates/web-app/tests/cockpit.test.mjs +73 -73
- package/templates/web-app/tests/field-selection.test.mjs +70 -70
- package/templates/web-app/tests/retail.test.mjs +31 -31
- package/templates/web-app/tests/table-normalize.test.mjs +189 -189
- package/templates/web-app/tsconfig.json +4 -4
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json
CHANGED
|
@@ -1,185 +1,185 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": "1",
|
|
3
|
-
"templateVersion": "0.3.0",
|
|
4
|
-
"title": "零售经营简报",
|
|
5
|
-
"description": "请根据同一来源的真实指标、字段和控件替换绑定。",
|
|
6
|
-
"basePath": "/webroot/decision",
|
|
7
|
-
"layout": "retail",
|
|
8
|
-
"dashboardId": "REPLACE_DASHBOARD_ID",
|
|
9
|
-
"directoryEntryId": "REPLACE_DIRECTORY_ENTRY_ID",
|
|
10
|
-
"maxRows": 1000,
|
|
11
|
-
"pageSize": 8,
|
|
12
|
-
"insights": false,
|
|
13
|
-
"widgets": [
|
|
14
|
-
{
|
|
15
|
-
"entryId": "REPLACE_ENTRY_1",
|
|
16
|
-
"name": "销售额",
|
|
17
|
-
"type": "kpi",
|
|
18
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
19
|
-
"filterNames": [],
|
|
20
|
-
"valueFieldId": "REPLACE_VALUE_1",
|
|
21
|
-
"valueFormat": "number"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"entryId": "REPLACE_ENTRY_2",
|
|
25
|
-
"name": "毛利",
|
|
26
|
-
"type": "kpi",
|
|
27
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
28
|
-
"filterNames": [],
|
|
29
|
-
"valueFieldId": "REPLACE_VALUE_2",
|
|
30
|
-
"valueFormat": "number"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"entryId": "REPLACE_ENTRY_3",
|
|
34
|
-
"name": "比率指标",
|
|
35
|
-
"type": "kpi",
|
|
36
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
37
|
-
"filterNames": [],
|
|
38
|
-
"valueFieldId": "REPLACE_VALUE_3",
|
|
39
|
-
"valueFormat": "percent"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"entryId": "REPLACE_ENTRY_4",
|
|
43
|
-
"name": "计数指标",
|
|
44
|
-
"type": "kpi",
|
|
45
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
46
|
-
"filterNames": [],
|
|
47
|
-
"valueFieldId": "REPLACE_VALUE_4",
|
|
48
|
-
"valueFormat": "number"
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"entryId": "REPLACE_ENTRY_5",
|
|
52
|
-
"name": "销售与毛利趋势",
|
|
53
|
-
"type": "line",
|
|
54
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
55
|
-
"filterNames": [],
|
|
56
|
-
"categoryFieldId": "REPLACE_CATEGORY_5",
|
|
57
|
-
"valueFieldId": "REPLACE_VALUE_5",
|
|
58
|
-
"categoryFormat": "epoch-month",
|
|
59
|
-
"categoryTimeZone": "Asia/Shanghai",
|
|
60
|
-
"series": [
|
|
61
|
-
{
|
|
62
|
-
"fieldId": "REPLACE_SERIES_5_1",
|
|
63
|
-
"label": "销售额"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"fieldId": "REPLACE_SERIES_5_2",
|
|
67
|
-
"label": "毛利"
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"entryId": "REPLACE_ENTRY_6",
|
|
73
|
-
"name": "销售额 TOP10 门店",
|
|
74
|
-
"type": "bar",
|
|
75
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
76
|
-
"filterNames": [],
|
|
77
|
-
"categoryFieldId": "REPLACE_CATEGORY_6",
|
|
78
|
-
"valueFieldId": "REPLACE_VALUE_6"
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
"entryId": "REPLACE_ENTRY_7",
|
|
82
|
-
"name": "区域销售构成",
|
|
83
|
-
"type": "pie",
|
|
84
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
85
|
-
"filterNames": [],
|
|
86
|
-
"categoryFieldId": "REPLACE_CATEGORY_7",
|
|
87
|
-
"valueFieldId": "REPLACE_VALUE_7"
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
"entryId": "REPLACE_ENTRY_8",
|
|
91
|
-
"name": "品类销售额",
|
|
92
|
-
"type": "bar",
|
|
93
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
94
|
-
"filterNames": [],
|
|
95
|
-
"categoryFieldId": "REPLACE_CATEGORY_8",
|
|
96
|
-
"valueFieldId": "REPLACE_VALUE_8"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
"entryId": "REPLACE_ENTRY_9",
|
|
100
|
-
"name": "自有店 / 管理店销售趋势",
|
|
101
|
-
"type": "line",
|
|
102
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
103
|
-
"filterNames": [],
|
|
104
|
-
"categoryFieldId": "REPLACE_CATEGORY_9",
|
|
105
|
-
"valueFieldId": "REPLACE_VALUE_9",
|
|
106
|
-
"categoryFormat": "epoch-month",
|
|
107
|
-
"categoryTimeZone": "Asia/Shanghai",
|
|
108
|
-
"series": [
|
|
109
|
-
{
|
|
110
|
-
"fieldId": "REPLACE_SERIES_9_1",
|
|
111
|
-
"label": "自有店"
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
"fieldId": "REPLACE_SERIES_9_2",
|
|
115
|
-
"label": "管理店"
|
|
116
|
-
}
|
|
117
|
-
]
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
"entryId": "REPLACE_ENTRY_10",
|
|
121
|
-
"name": "生活馆 / 时尚馆销售趋势",
|
|
122
|
-
"type": "line",
|
|
123
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
124
|
-
"filterNames": [],
|
|
125
|
-
"categoryFieldId": "REPLACE_CATEGORY_10",
|
|
126
|
-
"valueFieldId": "REPLACE_VALUE_10",
|
|
127
|
-
"categoryFormat": "epoch-month",
|
|
128
|
-
"categoryTimeZone": "Asia/Shanghai",
|
|
129
|
-
"series": [
|
|
130
|
-
{
|
|
131
|
-
"fieldId": "REPLACE_SERIES_10_1",
|
|
132
|
-
"label": "生活馆"
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
"fieldId": "REPLACE_SERIES_10_2",
|
|
136
|
-
"label": "时尚馆"
|
|
137
|
-
}
|
|
138
|
-
]
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
"entryId": "REPLACE_ENTRY_11",
|
|
142
|
-
"name": "门店风格构成",
|
|
143
|
-
"type": "pie",
|
|
144
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
145
|
-
"filterNames": [],
|
|
146
|
-
"categoryFieldId": "REPLACE_CATEGORY_11",
|
|
147
|
-
"valueFieldId": "REPLACE_VALUE_11"
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
"entryId": "REPLACE_ENTRY_12",
|
|
151
|
-
"name": "门店性质构成",
|
|
152
|
-
"type": "pie",
|
|
153
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
154
|
-
"filterNames": [],
|
|
155
|
-
"categoryFieldId": "REPLACE_CATEGORY_12",
|
|
156
|
-
"valueFieldId": "REPLACE_VALUE_12"
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
"entryId": "REPLACE_ENTRY_13",
|
|
160
|
-
"name": "门店经营明细",
|
|
161
|
-
"type": "table",
|
|
162
|
-
"description": "替换为已核实的源指标口径与范围。",
|
|
163
|
-
"filterNames": [],
|
|
164
|
-
"tableFieldIds": [
|
|
165
|
-
"REPLACE_TABLE_FIELD_1",
|
|
166
|
-
"REPLACE_TABLE_FIELD_2",
|
|
167
|
-
"REPLACE_TABLE_FIELD_3",
|
|
168
|
-
"REPLACE_TABLE_FIELD_4"
|
|
169
|
-
],
|
|
170
|
-
"localFilterFieldIds": [
|
|
171
|
-
"REPLACE_TABLE_FIELD_1"
|
|
172
|
-
]
|
|
173
|
-
}
|
|
174
|
-
],
|
|
175
|
-
"filters": [
|
|
176
|
-
{
|
|
177
|
-
"name": "REPLACE_STORE_CONTROL",
|
|
178
|
-
"label": "店名(精确匹配)",
|
|
179
|
-
"type": "text"
|
|
180
|
-
}
|
|
181
|
-
],
|
|
182
|
-
"dataNotes": [
|
|
183
|
-
"请填写已核实的来源、单位、期间、门店计数口径与控件作用范围。未确认公式时保留源指标名称。"
|
|
184
|
-
]
|
|
185
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1",
|
|
3
|
+
"templateVersion": "0.3.0",
|
|
4
|
+
"title": "零售经营简报",
|
|
5
|
+
"description": "请根据同一来源的真实指标、字段和控件替换绑定。",
|
|
6
|
+
"basePath": "/webroot/decision",
|
|
7
|
+
"layout": "retail",
|
|
8
|
+
"dashboardId": "REPLACE_DASHBOARD_ID",
|
|
9
|
+
"directoryEntryId": "REPLACE_DIRECTORY_ENTRY_ID",
|
|
10
|
+
"maxRows": 1000,
|
|
11
|
+
"pageSize": 8,
|
|
12
|
+
"insights": false,
|
|
13
|
+
"widgets": [
|
|
14
|
+
{
|
|
15
|
+
"entryId": "REPLACE_ENTRY_1",
|
|
16
|
+
"name": "销售额",
|
|
17
|
+
"type": "kpi",
|
|
18
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
19
|
+
"filterNames": [],
|
|
20
|
+
"valueFieldId": "REPLACE_VALUE_1",
|
|
21
|
+
"valueFormat": "number"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"entryId": "REPLACE_ENTRY_2",
|
|
25
|
+
"name": "毛利",
|
|
26
|
+
"type": "kpi",
|
|
27
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
28
|
+
"filterNames": [],
|
|
29
|
+
"valueFieldId": "REPLACE_VALUE_2",
|
|
30
|
+
"valueFormat": "number"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"entryId": "REPLACE_ENTRY_3",
|
|
34
|
+
"name": "比率指标",
|
|
35
|
+
"type": "kpi",
|
|
36
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
37
|
+
"filterNames": [],
|
|
38
|
+
"valueFieldId": "REPLACE_VALUE_3",
|
|
39
|
+
"valueFormat": "percent"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"entryId": "REPLACE_ENTRY_4",
|
|
43
|
+
"name": "计数指标",
|
|
44
|
+
"type": "kpi",
|
|
45
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
46
|
+
"filterNames": [],
|
|
47
|
+
"valueFieldId": "REPLACE_VALUE_4",
|
|
48
|
+
"valueFormat": "number"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"entryId": "REPLACE_ENTRY_5",
|
|
52
|
+
"name": "销售与毛利趋势",
|
|
53
|
+
"type": "line",
|
|
54
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
55
|
+
"filterNames": [],
|
|
56
|
+
"categoryFieldId": "REPLACE_CATEGORY_5",
|
|
57
|
+
"valueFieldId": "REPLACE_VALUE_5",
|
|
58
|
+
"categoryFormat": "epoch-month",
|
|
59
|
+
"categoryTimeZone": "Asia/Shanghai",
|
|
60
|
+
"series": [
|
|
61
|
+
{
|
|
62
|
+
"fieldId": "REPLACE_SERIES_5_1",
|
|
63
|
+
"label": "销售额"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"fieldId": "REPLACE_SERIES_5_2",
|
|
67
|
+
"label": "毛利"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"entryId": "REPLACE_ENTRY_6",
|
|
73
|
+
"name": "销售额 TOP10 门店",
|
|
74
|
+
"type": "bar",
|
|
75
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
76
|
+
"filterNames": [],
|
|
77
|
+
"categoryFieldId": "REPLACE_CATEGORY_6",
|
|
78
|
+
"valueFieldId": "REPLACE_VALUE_6"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"entryId": "REPLACE_ENTRY_7",
|
|
82
|
+
"name": "区域销售构成",
|
|
83
|
+
"type": "pie",
|
|
84
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
85
|
+
"filterNames": [],
|
|
86
|
+
"categoryFieldId": "REPLACE_CATEGORY_7",
|
|
87
|
+
"valueFieldId": "REPLACE_VALUE_7"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"entryId": "REPLACE_ENTRY_8",
|
|
91
|
+
"name": "品类销售额",
|
|
92
|
+
"type": "bar",
|
|
93
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
94
|
+
"filterNames": [],
|
|
95
|
+
"categoryFieldId": "REPLACE_CATEGORY_8",
|
|
96
|
+
"valueFieldId": "REPLACE_VALUE_8"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"entryId": "REPLACE_ENTRY_9",
|
|
100
|
+
"name": "自有店 / 管理店销售趋势",
|
|
101
|
+
"type": "line",
|
|
102
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
103
|
+
"filterNames": [],
|
|
104
|
+
"categoryFieldId": "REPLACE_CATEGORY_9",
|
|
105
|
+
"valueFieldId": "REPLACE_VALUE_9",
|
|
106
|
+
"categoryFormat": "epoch-month",
|
|
107
|
+
"categoryTimeZone": "Asia/Shanghai",
|
|
108
|
+
"series": [
|
|
109
|
+
{
|
|
110
|
+
"fieldId": "REPLACE_SERIES_9_1",
|
|
111
|
+
"label": "自有店"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"fieldId": "REPLACE_SERIES_9_2",
|
|
115
|
+
"label": "管理店"
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"entryId": "REPLACE_ENTRY_10",
|
|
121
|
+
"name": "生活馆 / 时尚馆销售趋势",
|
|
122
|
+
"type": "line",
|
|
123
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
124
|
+
"filterNames": [],
|
|
125
|
+
"categoryFieldId": "REPLACE_CATEGORY_10",
|
|
126
|
+
"valueFieldId": "REPLACE_VALUE_10",
|
|
127
|
+
"categoryFormat": "epoch-month",
|
|
128
|
+
"categoryTimeZone": "Asia/Shanghai",
|
|
129
|
+
"series": [
|
|
130
|
+
{
|
|
131
|
+
"fieldId": "REPLACE_SERIES_10_1",
|
|
132
|
+
"label": "生活馆"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"fieldId": "REPLACE_SERIES_10_2",
|
|
136
|
+
"label": "时尚馆"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"entryId": "REPLACE_ENTRY_11",
|
|
142
|
+
"name": "门店风格构成",
|
|
143
|
+
"type": "pie",
|
|
144
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
145
|
+
"filterNames": [],
|
|
146
|
+
"categoryFieldId": "REPLACE_CATEGORY_11",
|
|
147
|
+
"valueFieldId": "REPLACE_VALUE_11"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"entryId": "REPLACE_ENTRY_12",
|
|
151
|
+
"name": "门店性质构成",
|
|
152
|
+
"type": "pie",
|
|
153
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
154
|
+
"filterNames": [],
|
|
155
|
+
"categoryFieldId": "REPLACE_CATEGORY_12",
|
|
156
|
+
"valueFieldId": "REPLACE_VALUE_12"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"entryId": "REPLACE_ENTRY_13",
|
|
160
|
+
"name": "门店经营明细",
|
|
161
|
+
"type": "table",
|
|
162
|
+
"description": "替换为已核实的源指标口径与范围。",
|
|
163
|
+
"filterNames": [],
|
|
164
|
+
"tableFieldIds": [
|
|
165
|
+
"REPLACE_TABLE_FIELD_1",
|
|
166
|
+
"REPLACE_TABLE_FIELD_2",
|
|
167
|
+
"REPLACE_TABLE_FIELD_3",
|
|
168
|
+
"REPLACE_TABLE_FIELD_4"
|
|
169
|
+
],
|
|
170
|
+
"localFilterFieldIds": [
|
|
171
|
+
"REPLACE_TABLE_FIELD_1"
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"filters": [
|
|
176
|
+
{
|
|
177
|
+
"name": "REPLACE_STORE_CONTROL",
|
|
178
|
+
"label": "店名(精确匹配)",
|
|
179
|
+
"type": "text"
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"dataNotes": [
|
|
183
|
+
"请填写已核实的来源、单位、期间、门店计数口径与控件作用范围。未确认公式时保留源指标名称。"
|
|
184
|
+
]
|
|
185
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>FineBI 数据应用</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>FineBI 数据应用</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": "1",
|
|
3
|
-
"title": "FineBI 数据应用",
|
|
4
|
-
"basePath": "/webroot/decision",
|
|
5
|
-
"layout": "overview",
|
|
6
|
-
"dashboardId": "",
|
|
7
|
-
"maxRows": 1000,
|
|
8
|
-
"pageSize": 8,
|
|
9
|
-
"widgets": [],
|
|
10
|
-
"filters": []
|
|
11
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1",
|
|
3
|
+
"title": "FineBI 数据应用",
|
|
4
|
+
"basePath": "/webroot/decision",
|
|
5
|
+
"layout": "overview",
|
|
6
|
+
"dashboardId": "",
|
|
7
|
+
"maxRows": 1000,
|
|
8
|
+
"pageSize": 8,
|
|
9
|
+
"widgets": [],
|
|
10
|
+
"filters": []
|
|
11
|
+
}
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import {lazy,Suspense,useEffect,useRef,useState} from 'react';
|
|
2
|
-
import {queryWidget,validateConfig} from './finebi/client';
|
|
3
|
-
import type {AppConfig,FilterState,WidgetBinding} from './finebi/types';
|
|
4
|
-
import {Boundary,Widget} from './components/Widget';
|
|
5
|
-
import {Insights} from './components/Insights';
|
|
6
|
-
import {loadWidgets,widgetKey,type WidgetState} from './services/widget-queries';
|
|
7
|
-
import './cockpit.css';
|
|
8
|
-
import './retail.css';
|
|
9
|
-
const FilterBar=lazy(()=>import('./components/FilterBar').then(module=>({default:module.FilterBar})));
|
|
10
|
-
function Dashboard({config}: {config: AppConfig}) {
|
|
11
|
-
const [filters,setFilters]=useState<FilterState>({}),[refresh,setRefresh]=useState(0);
|
|
12
|
-
const [states,setStates]=useState<Record<string,WidgetState>>({});
|
|
13
|
-
const [activeHash,setActiveHash]=useState(()=>window.location.hash||'#dashboard');
|
|
14
|
-
useEffect(()=>{
|
|
15
|
-
const syncHash=()=>setActiveHash(window.location.hash||'#dashboard');
|
|
16
|
-
window.addEventListener('hashchange',syncHash);
|
|
17
|
-
syncHash();
|
|
18
|
-
return ()=>window.removeEventListener('hashchange',syncHash);
|
|
19
|
-
},[]);
|
|
20
|
-
const generation=useRef(0),retries=useRef(new Map<string,AbortController>());
|
|
21
|
-
useEffect(()=>{
|
|
22
|
-
const controller=new AbortController();generation.current++;setStates({});
|
|
23
|
-
void loadWidgets(config,filters,controller.signal,(key,state)=>setStates(old=>({...old,[key]:state})));
|
|
24
|
-
return ()=>{controller.abort();retries.current.forEach(c=>c.abort());retries.current.clear();};
|
|
25
|
-
},[config,filters,refresh]);
|
|
26
|
-
const retry=async(binding:WidgetBinding,index:number)=>{
|
|
27
|
-
const key=widgetKey(binding,index),current=generation.current,controller=new AbortController();
|
|
28
|
-
retries.current.get(key)?.abort();retries.current.set(key,controller);
|
|
29
|
-
setStates(old=>({...old,[key]:{loading:true}}));
|
|
30
|
-
try {const result=await queryWidget(config,binding,filters,controller.signal);if(current===generation.current&&!controller.signal.aborted)setStates(old=>({...old,[key]:{loading:false,result,at:new Date().toLocaleTimeString()}}));}
|
|
31
|
-
catch(error){if(current===generation.current&&!controller.signal.aborted)setStates(old=>({...old,[key]:{loading:false,error:error as Error}}));}
|
|
32
|
-
};
|
|
33
|
-
const complete=Object.values(states).filter(s=>s.result).length,failed=Object.values(states).filter(s=>s.error).length;
|
|
34
|
-
const scope=Object.fromEntries(config.filters.map(filter=>{
|
|
35
|
-
const known=config.widgets.filter(w=>w.filterNames?.includes(filter.name)).map(w=>w.name);
|
|
36
|
-
const unknown=config.widgets.some(w=>w.filterNames===undefined);
|
|
37
|
-
return [filter.name,`${known.length?known.join('、'):'无已确认组件'}${unknown?';未映射组件由源看板决定':''}`];
|
|
38
|
-
}));
|
|
39
|
-
const retail=config.layout==='retail';
|
|
40
|
-
const card=(binding:WidgetBinding,index:number)=><Widget key={`${refresh}-${JSON.stringify(filters)}-${index}`} binding={binding} state={states[widgetKey(binding,index)]??{loading:true}} pageSize={config.pageSize} onRetry={()=>void retry(binding,index)} compact={retail}/>;
|
|
41
|
-
const cockpit=config.layout==='cockpit';
|
|
42
|
-
const separateKpis=cockpit||retail;
|
|
43
|
-
return <div className={`app ${config.layout}`}>
|
|
44
|
-
{retail&&<nav className="retail-nav" aria-label="页面导航"><a className="retail-brand" href="#dashboard"><b>F.</b><span>FineBI <small>经营简报</small></span></a><div><a href="#dashboard" aria-current={activeHash==='#dashboard'?'location':undefined}>经营概览</a><a href="#components" aria-current={activeHash==='#components'?'location':undefined}>销售分析</a><a href="#data-notes" aria-current={activeHash==='#data-notes'?'location':undefined}>口径说明</a></div><span className="retail-live">动态数据</span></nav>}
|
|
45
|
-
{config.layout!=='overview'&&!retail&&<aside><strong>FineBI<span>数据应用</span></strong><nav><a href="#dashboard">数据概览</a><a href="#components">分析组件</a>{config.insights&&<a href="#observations">经营观察</a>}<a href="#data-notes">数据口径</a></nav><p className="source-label">{config.directoryEntryId?'目录发布来源':'分析主题来源'}<br/>访问者权限由 FineBI 控制</p></aside>}
|
|
46
|
-
<main id="dashboard"><header><div><span className="eyebrow">FINEBI · LIVE DATA</span><h1>{config.title}</h1><p className="muted">{config.description??'基于源看板的实时数据,按当前访问权限呈现'}</p></div><div className="header-actions"><a href={retail?'#data-notes':config.basePath} target={retail?undefined:'_blank'} rel="noreferrer">{retail?'口径说明':'打开 FineBI'}</a><button onClick={()=>setRefresh(n=>n+1)}>刷新数据</button></div></header>
|
|
47
|
-
{config.filters.length>0&&<Boundary><Suspense fallback={<p role="status">正在加载筛选控件…</p>}><FilterBar bindings={config.filters} onApply={setFilters} cockpit={cockpit} scope={scope} compact={retail}/></Suspense></Boundary>}
|
|
48
|
-
<p className="live query-status" role="status">{config.widgets.length} 个展示组件 · 已返回 {complete} · 失败 {failed} · 其余加载中或等待查询</p>
|
|
49
|
-
{retail&&<div className="section-heading"><span>01 / OVERVIEW</span><h2>经营概览</h2><p>源看板当前条件 · 随查询更新</p></div>}
|
|
50
|
-
{separateKpis&&<div className="kpi-grid">{config.widgets.map((binding,i)=>binding.type==='kpi'?card(binding,i):null)}</div>}
|
|
51
|
-
{config.insights&&<Insights config={config} states={states}/>}
|
|
52
|
-
{retail&&<div className="section-heading" id="components"><span>02 / PERFORMANCE</span><h2>销售表现</h2><p>趋势、构成与门店表现</p></div>}
|
|
53
|
-
<div className="grid" id={retail?undefined:'components'}>{config.widgets.map((binding,i)=>!separateKpis||binding.type!=='kpi'?card(binding,i):null)}</div>
|
|
54
|
-
<details className="data-notes" id="data-notes"><summary>数据口径与范围</summary><p>当前仅消费单次返回结果;COMPLETE 表示本次转换完整,不保证全量。搜索、排序、筛选、分页和 CSV 仅作用于已加载数据。UNKNOWN 行不自动累加,受限值与空值不补零。</p>{config.dataNotes?.map((note,i)=><p key={i}>{note}</p>)}<p>{config.directoryEntryId?'目录入口已绑定;查询失败不会切换到其他来源。':'来源为当前配置的分析看板。'}</p></details>
|
|
55
|
-
<footer>数据范围以当前查询结果为准 · 权限由 FineBI 服务端控制</footer></main>
|
|
56
|
-
</div>;
|
|
57
|
-
}
|
|
58
|
-
export default function App() {
|
|
59
|
-
const [config, setConfig] = useState<AppConfig>(); const [error, setError] = useState('');
|
|
60
|
-
useEffect(()=>{const controller=new AbortController(); fetch('./app-config.json',{signal:controller.signal,cache:'no-store'}).then(r=>{if(!r.ok)throw Error('应用配置加载失败');return r.json();}).then(c=>{validateConfig(c);setConfig(c);}).catch(e=>{if(!controller.signal.aborted)setError(e.message);});return()=>controller.abort();},[]);
|
|
61
|
-
if(error)return <main className="state" role="alert"><h1>请完成应用配置</h1><p>{error}</p><p>在 public/app-config.json 中绑定 CLI 发现的仪表板实例与字段。</p></main>;
|
|
62
|
-
return config ? <Dashboard config={config}/> : <p className="state">正在加载应用…</p>;
|
|
63
|
-
}
|
|
1
|
+
import {lazy,Suspense,useEffect,useRef,useState} from 'react';
|
|
2
|
+
import {queryWidget,validateConfig} from './finebi/client';
|
|
3
|
+
import type {AppConfig,FilterState,WidgetBinding} from './finebi/types';
|
|
4
|
+
import {Boundary,Widget} from './components/Widget';
|
|
5
|
+
import {Insights} from './components/Insights';
|
|
6
|
+
import {loadWidgets,widgetKey,type WidgetState} from './services/widget-queries';
|
|
7
|
+
import './cockpit.css';
|
|
8
|
+
import './retail.css';
|
|
9
|
+
const FilterBar=lazy(()=>import('./components/FilterBar').then(module=>({default:module.FilterBar})));
|
|
10
|
+
function Dashboard({config}: {config: AppConfig}) {
|
|
11
|
+
const [filters,setFilters]=useState<FilterState>({}),[refresh,setRefresh]=useState(0);
|
|
12
|
+
const [states,setStates]=useState<Record<string,WidgetState>>({});
|
|
13
|
+
const [activeHash,setActiveHash]=useState(()=>window.location.hash||'#dashboard');
|
|
14
|
+
useEffect(()=>{
|
|
15
|
+
const syncHash=()=>setActiveHash(window.location.hash||'#dashboard');
|
|
16
|
+
window.addEventListener('hashchange',syncHash);
|
|
17
|
+
syncHash();
|
|
18
|
+
return ()=>window.removeEventListener('hashchange',syncHash);
|
|
19
|
+
},[]);
|
|
20
|
+
const generation=useRef(0),retries=useRef(new Map<string,AbortController>());
|
|
21
|
+
useEffect(()=>{
|
|
22
|
+
const controller=new AbortController();generation.current++;setStates({});
|
|
23
|
+
void loadWidgets(config,filters,controller.signal,(key,state)=>setStates(old=>({...old,[key]:state})));
|
|
24
|
+
return ()=>{controller.abort();retries.current.forEach(c=>c.abort());retries.current.clear();};
|
|
25
|
+
},[config,filters,refresh]);
|
|
26
|
+
const retry=async(binding:WidgetBinding,index:number)=>{
|
|
27
|
+
const key=widgetKey(binding,index),current=generation.current,controller=new AbortController();
|
|
28
|
+
retries.current.get(key)?.abort();retries.current.set(key,controller);
|
|
29
|
+
setStates(old=>({...old,[key]:{loading:true}}));
|
|
30
|
+
try {const result=await queryWidget(config,binding,filters,controller.signal);if(current===generation.current&&!controller.signal.aborted)setStates(old=>({...old,[key]:{loading:false,result,at:new Date().toLocaleTimeString()}}));}
|
|
31
|
+
catch(error){if(current===generation.current&&!controller.signal.aborted)setStates(old=>({...old,[key]:{loading:false,error:error as Error}}));}
|
|
32
|
+
};
|
|
33
|
+
const complete=Object.values(states).filter(s=>s.result).length,failed=Object.values(states).filter(s=>s.error).length;
|
|
34
|
+
const scope=Object.fromEntries(config.filters.map(filter=>{
|
|
35
|
+
const known=config.widgets.filter(w=>w.filterNames?.includes(filter.name)).map(w=>w.name);
|
|
36
|
+
const unknown=config.widgets.some(w=>w.filterNames===undefined);
|
|
37
|
+
return [filter.name,`${known.length?known.join('、'):'无已确认组件'}${unknown?';未映射组件由源看板决定':''}`];
|
|
38
|
+
}));
|
|
39
|
+
const retail=config.layout==='retail';
|
|
40
|
+
const card=(binding:WidgetBinding,index:number)=><Widget key={`${refresh}-${JSON.stringify(filters)}-${index}`} binding={binding} state={states[widgetKey(binding,index)]??{loading:true}} pageSize={config.pageSize} onRetry={()=>void retry(binding,index)} compact={retail}/>;
|
|
41
|
+
const cockpit=config.layout==='cockpit';
|
|
42
|
+
const separateKpis=cockpit||retail;
|
|
43
|
+
return <div className={`app ${config.layout}`}>
|
|
44
|
+
{retail&&<nav className="retail-nav" aria-label="页面导航"><a className="retail-brand" href="#dashboard"><b>F.</b><span>FineBI <small>经营简报</small></span></a><div><a href="#dashboard" aria-current={activeHash==='#dashboard'?'location':undefined}>经营概览</a><a href="#components" aria-current={activeHash==='#components'?'location':undefined}>销售分析</a><a href="#data-notes" aria-current={activeHash==='#data-notes'?'location':undefined}>口径说明</a></div><span className="retail-live">动态数据</span></nav>}
|
|
45
|
+
{config.layout!=='overview'&&!retail&&<aside><strong>FineBI<span>数据应用</span></strong><nav><a href="#dashboard">数据概览</a><a href="#components">分析组件</a>{config.insights&&<a href="#observations">经营观察</a>}<a href="#data-notes">数据口径</a></nav><p className="source-label">{config.directoryEntryId?'目录发布来源':'分析主题来源'}<br/>访问者权限由 FineBI 控制</p></aside>}
|
|
46
|
+
<main id="dashboard"><header><div><span className="eyebrow">FINEBI · LIVE DATA</span><h1>{config.title}</h1><p className="muted">{config.description??'基于源看板的实时数据,按当前访问权限呈现'}</p></div><div className="header-actions"><a href={retail?'#data-notes':config.basePath} target={retail?undefined:'_blank'} rel="noreferrer">{retail?'口径说明':'打开 FineBI'}</a><button onClick={()=>setRefresh(n=>n+1)}>刷新数据</button></div></header>
|
|
47
|
+
{config.filters.length>0&&<Boundary><Suspense fallback={<p role="status">正在加载筛选控件…</p>}><FilterBar bindings={config.filters} onApply={setFilters} cockpit={cockpit} scope={scope} compact={retail}/></Suspense></Boundary>}
|
|
48
|
+
<p className="live query-status" role="status">{config.widgets.length} 个展示组件 · 已返回 {complete} · 失败 {failed} · 其余加载中或等待查询</p>
|
|
49
|
+
{retail&&<div className="section-heading"><span>01 / OVERVIEW</span><h2>经营概览</h2><p>源看板当前条件 · 随查询更新</p></div>}
|
|
50
|
+
{separateKpis&&<div className="kpi-grid">{config.widgets.map((binding,i)=>binding.type==='kpi'?card(binding,i):null)}</div>}
|
|
51
|
+
{config.insights&&<Insights config={config} states={states}/>}
|
|
52
|
+
{retail&&<div className="section-heading" id="components"><span>02 / PERFORMANCE</span><h2>销售表现</h2><p>趋势、构成与门店表现</p></div>}
|
|
53
|
+
<div className="grid" id={retail?undefined:'components'}>{config.widgets.map((binding,i)=>!separateKpis||binding.type!=='kpi'?card(binding,i):null)}</div>
|
|
54
|
+
<details className="data-notes" id="data-notes"><summary>数据口径与范围</summary><p>当前仅消费单次返回结果;COMPLETE 表示本次转换完整,不保证全量。搜索、排序、筛选、分页和 CSV 仅作用于已加载数据。UNKNOWN 行不自动累加,受限值与空值不补零。</p>{config.dataNotes?.map((note,i)=><p key={i}>{note}</p>)}<p>{config.directoryEntryId?'目录入口已绑定;查询失败不会切换到其他来源。':'来源为当前配置的分析看板。'}</p></details>
|
|
55
|
+
<footer>数据范围以当前查询结果为准 · 权限由 FineBI 服务端控制</footer></main>
|
|
56
|
+
</div>;
|
|
57
|
+
}
|
|
58
|
+
export default function App() {
|
|
59
|
+
const [config, setConfig] = useState<AppConfig>(); const [error, setError] = useState('');
|
|
60
|
+
useEffect(()=>{const controller=new AbortController(); fetch('./app-config.json',{signal:controller.signal,cache:'no-store'}).then(r=>{if(!r.ok)throw Error('应用配置加载失败');return r.json();}).then(c=>{validateConfig(c);setConfig(c);}).catch(e=>{if(!controller.signal.aborted)setError(e.message);});return()=>controller.abort();},[]);
|
|
61
|
+
if(error)return <main className="state" role="alert"><h1>请完成应用配置</h1><p>{error}</p><p>在 public/app-config.json 中绑定 CLI 发现的仪表板实例与字段。</p></main>;
|
|
62
|
+
return config ? <Dashboard config={config}/> : <p className="state">正在加载应用…</p>;
|
|
63
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
.panel-heading{display:flex;align-items:start;justify-content:space-between;gap:12px}.panel-heading h2{margin-bottom:6px}.component-status{font-size:10px;color:#71867d;white-space:nowrap}.component-status.failed{color:#bd534f}.result-range,.filter-scope{font-size:11px;color:#84928b;overflow-wrap:anywhere}.filter-scope{margin:4px 0 10px}.component-error{padding:14px 0;color:#af4e46;overflow-wrap:anywhere}.component-error button{color:#8f5044}.header-actions{display:flex;gap:18px;align-items:center;white-space:nowrap}.insight-panel{display:grid;grid-template-columns:220px minmax(0,1fr);gap:28px;background:#edf6f1;border:1px solid #dbece2;border-radius:15px;padding:24px;margin:24px 0}.insight-panel h2{margin:8px 0}.insight-panel p{font-size:12px;color:#6b8576}.insight-panel ul{margin:0;padding-left:18px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 24px;font-size:12px;color:#37604a}.data-notes{background:#fff;padding:20px 24px;border:1px solid #e2e8e3;border-radius:12px;margin-top:24px;font-size:12px}.sort-column{border:0;background:transparent;padding:0;font-weight:600;font-size:inherit}.local-filters,.series-controls{display:flex;flex-wrap:wrap;gap:14px;align-items:center;margin-bottom:16px}.local-filters label{font-size:12px;color:#64748b}.local-filters select{margin-left:8px;max-width:220px}.series-controls{font-size:12px}.series-controls label{display:flex;gap:6px;align-items:center}.workbench aside strong span{display:block;font-size:12px;font-weight:400}.source-label{font-size:11px;line-height:1.9;opacity:.65;margin-top:45px}
|
|
2
|
-
.cockpit{display:grid;grid-template-columns:208px minmax(0,1fr);min-height:100vh;background:#f3f6f3;color:#253f33}.cockpit aside{background:#123d2c;color:#e0ece4;padding:38px 24px;position:sticky;top:0;height:100vh;min-height:0;display:flex;flex-direction:column}.cockpit aside strong{font-size:27px;letter-spacing:-1px}.cockpit aside strong span{display:block;font-size:11px;letter-spacing:3px;font-weight:400;margin-top:3px;color:#b1c6b8}.cockpit aside nav{margin-top:48px}.cockpit aside a{display:block;font-size:13px;margin:8px -8px;padding:12px 16px;color:#c5d5ca;border-radius:8px;transition:background .15s}.cockpit aside a:hover{background:#ffffff10;color:white}.cockpit aside a:first-child{background:#ffffff13;color:#fff}.cockpit .source-label{margin-top:auto;padding-top:40px}.cockpit main{width:100%;max-width:1640px;padding:38px 40px}.cockpit header{padding-bottom:22px;border-bottom:1px solid #dfe8df;margin-bottom:26px}.cockpit h1{font-size:29px;letter-spacing:-.8px;margin:8px 0}.cockpit h2{font-size:15px;font-weight:650}.cockpit .eyebrow{font-size:10px;color:#659476;letter-spacing:2.5px}.cockpit .muted{font-size:12px;color:#839186}.cockpit a{color:#35775a}.cockpit button:hover{border-color:#197a58}.cockpit .header-actions>button{background:#197a58;border-color:#197a58;color:white;padding:10px 18px}.cockpit .grid{gap:20px}.cockpit .panel{border:1px solid #e0e8df;border-radius:14px;padding:24px;box-shadow:0 3px 12px #193f2103}.cockpit .kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px}.cockpit .widget-kpi{position:relative;overflow:hidden}.cockpit .widget-kpi:before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:#448b64}.cockpit .widget-kpi:nth-child(2):before{background:#588db2}.cockpit .widget-kpi:nth-child(3):before{background:#d2ad60}.cockpit .widget-kpi:nth-child(4):before{background:#8f7db0}.cockpit .widget-kpi .panel-heading{display:block}.cockpit .widget-kpi .component-status{font-size:9px}.cockpit .kpi{font-size:clamp(22px,2.3vw,36px);color:#24533b;font-variant-numeric:tabular-nums;overflow-wrap:anywhere;margin:15px 0 6px}.cockpit .widget-kpi .result-range{font-size:9px}.cockpit .filter-card{border-color:#dfe8df;border-radius:14px}.cockpit .filter-symbol{color:#377d57;background:#edf5ef}.cockpit .filter-heading h2,.cockpit .filter-label-row label{color:#35533f}.cockpit .query-status{margin:18px 0;color:#6a8976;font-size:11px}.cockpit footer{font-size:11px;color:#8b9b8f}.cockpit .notice{border-radius:5px;font-size:11px}.cockpit .chart{height:290px}.cockpit th{background:#f5f8f4;color:#54725d}.cockpit tbody tr:hover{background:#f5f8f4}.cockpit .table-tools{font-size:11px;flex-wrap:wrap}.cockpit .table-tools input{max-width:220px}.cockpit .pager{font-size:12px}.cockpit .data-notes{color:#6d8171}
|
|
3
|
-
@media(min-width:1500px){.cockpit main{padding:42px 54px}}@media(max-width:1150px){.cockpit{grid-template-columns:170px minmax(0,1fr)}.cockpit main{padding:28px 24px}.cockpit .kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.insight-panel{grid-template-columns:1fr;gap:8px}}@media(max-width:800px){.cockpit{grid-template-columns:1fr}.cockpit aside{display:none}.cockpit main{padding:24px 18px}.cockpit .grid{grid-template-columns:1fr}.cockpit header{align-items:flex-start;gap:20px;flex-direction:column}.cockpit h1{font-size:25px}.cockpit .filter-card{padding:20px 18px}.insight-panel ul{grid-template-columns:1fr}.cockpit .panel{padding:20px}.header-actions{font-size:12px}.local-filters{align-items:stretch;flex-direction:column}.cockpit .kpi-grid{gap:12px}.cockpit .kpi-grid .panel{padding:17px 14px}.cockpit .kpi{font-size:24px}.table-tools input{max-width:180px}.panel-heading{flex-wrap:wrap}.component-status{white-space:normal}}@media(prefers-reduced-motion:reduce){.cockpit *{transition:none!important;scroll-behavior:auto!important}}
|
|
1
|
+
.panel-heading{display:flex;align-items:start;justify-content:space-between;gap:12px}.panel-heading h2{margin-bottom:6px}.component-status{font-size:10px;color:#71867d;white-space:nowrap}.component-status.failed{color:#bd534f}.result-range,.filter-scope{font-size:11px;color:#84928b;overflow-wrap:anywhere}.filter-scope{margin:4px 0 10px}.component-error{padding:14px 0;color:#af4e46;overflow-wrap:anywhere}.component-error button{color:#8f5044}.header-actions{display:flex;gap:18px;align-items:center;white-space:nowrap}.insight-panel{display:grid;grid-template-columns:220px minmax(0,1fr);gap:28px;background:#edf6f1;border:1px solid #dbece2;border-radius:15px;padding:24px;margin:24px 0}.insight-panel h2{margin:8px 0}.insight-panel p{font-size:12px;color:#6b8576}.insight-panel ul{margin:0;padding-left:18px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 24px;font-size:12px;color:#37604a}.data-notes{background:#fff;padding:20px 24px;border:1px solid #e2e8e3;border-radius:12px;margin-top:24px;font-size:12px}.sort-column{border:0;background:transparent;padding:0;font-weight:600;font-size:inherit}.local-filters,.series-controls{display:flex;flex-wrap:wrap;gap:14px;align-items:center;margin-bottom:16px}.local-filters label{font-size:12px;color:#64748b}.local-filters select{margin-left:8px;max-width:220px}.series-controls{font-size:12px}.series-controls label{display:flex;gap:6px;align-items:center}.workbench aside strong span{display:block;font-size:12px;font-weight:400}.source-label{font-size:11px;line-height:1.9;opacity:.65;margin-top:45px}
|
|
2
|
+
.cockpit{display:grid;grid-template-columns:208px minmax(0,1fr);min-height:100vh;background:#f3f6f3;color:#253f33}.cockpit aside{background:#123d2c;color:#e0ece4;padding:38px 24px;position:sticky;top:0;height:100vh;min-height:0;display:flex;flex-direction:column}.cockpit aside strong{font-size:27px;letter-spacing:-1px}.cockpit aside strong span{display:block;font-size:11px;letter-spacing:3px;font-weight:400;margin-top:3px;color:#b1c6b8}.cockpit aside nav{margin-top:48px}.cockpit aside a{display:block;font-size:13px;margin:8px -8px;padding:12px 16px;color:#c5d5ca;border-radius:8px;transition:background .15s}.cockpit aside a:hover{background:#ffffff10;color:white}.cockpit aside a:first-child{background:#ffffff13;color:#fff}.cockpit .source-label{margin-top:auto;padding-top:40px}.cockpit main{width:100%;max-width:1640px;padding:38px 40px}.cockpit header{padding-bottom:22px;border-bottom:1px solid #dfe8df;margin-bottom:26px}.cockpit h1{font-size:29px;letter-spacing:-.8px;margin:8px 0}.cockpit h2{font-size:15px;font-weight:650}.cockpit .eyebrow{font-size:10px;color:#659476;letter-spacing:2.5px}.cockpit .muted{font-size:12px;color:#839186}.cockpit a{color:#35775a}.cockpit button:hover{border-color:#197a58}.cockpit .header-actions>button{background:#197a58;border-color:#197a58;color:white;padding:10px 18px}.cockpit .grid{gap:20px}.cockpit .panel{border:1px solid #e0e8df;border-radius:14px;padding:24px;box-shadow:0 3px 12px #193f2103}.cockpit .kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px}.cockpit .widget-kpi{position:relative;overflow:hidden}.cockpit .widget-kpi:before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:#448b64}.cockpit .widget-kpi:nth-child(2):before{background:#588db2}.cockpit .widget-kpi:nth-child(3):before{background:#d2ad60}.cockpit .widget-kpi:nth-child(4):before{background:#8f7db0}.cockpit .widget-kpi .panel-heading{display:block}.cockpit .widget-kpi .component-status{font-size:9px}.cockpit .kpi{font-size:clamp(22px,2.3vw,36px);color:#24533b;font-variant-numeric:tabular-nums;overflow-wrap:anywhere;margin:15px 0 6px}.cockpit .widget-kpi .result-range{font-size:9px}.cockpit .filter-card{border-color:#dfe8df;border-radius:14px}.cockpit .filter-symbol{color:#377d57;background:#edf5ef}.cockpit .filter-heading h2,.cockpit .filter-label-row label{color:#35533f}.cockpit .query-status{margin:18px 0;color:#6a8976;font-size:11px}.cockpit footer{font-size:11px;color:#8b9b8f}.cockpit .notice{border-radius:5px;font-size:11px}.cockpit .chart{height:290px}.cockpit th{background:#f5f8f4;color:#54725d}.cockpit tbody tr:hover{background:#f5f8f4}.cockpit .table-tools{font-size:11px;flex-wrap:wrap}.cockpit .table-tools input{max-width:220px}.cockpit .pager{font-size:12px}.cockpit .data-notes{color:#6d8171}
|
|
3
|
+
@media(min-width:1500px){.cockpit main{padding:42px 54px}}@media(max-width:1150px){.cockpit{grid-template-columns:170px minmax(0,1fr)}.cockpit main{padding:28px 24px}.cockpit .kpi-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.insight-panel{grid-template-columns:1fr;gap:8px}}@media(max-width:800px){.cockpit{grid-template-columns:1fr}.cockpit aside{display:none}.cockpit main{padding:24px 18px}.cockpit .grid{grid-template-columns:1fr}.cockpit header{align-items:flex-start;gap:20px;flex-direction:column}.cockpit h1{font-size:25px}.cockpit .filter-card{padding:20px 18px}.insight-panel ul{grid-template-columns:1fr}.cockpit .panel{padding:20px}.header-actions{font-size:12px}.local-filters{align-items:stretch;flex-direction:column}.cockpit .kpi-grid{gap:12px}.cockpit .kpi-grid .panel{padding:17px 14px}.cockpit .kpi{font-size:24px}.table-tools input{max-width:180px}.panel-heading{flex-wrap:wrap}.component-status{white-space:normal}}@media(prefers-reduced-motion:reduce){.cockpit *{transition:none!important;scroll-behavior:auto!important}}
|