@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.
Files changed (109) hide show
  1. package/README.md +3 -1
  2. package/agent-installation-guide.md +70 -0
  3. package/package.json +2 -1
  4. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  5. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +44 -44
  6. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  7. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +160 -157
  8. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
  9. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +55 -55
  10. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  11. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  12. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +95 -94
  13. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -80
  14. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/cockpit.config.json +22 -22
  15. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/examples/retail.config.json +185 -185
  16. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/gitignore.template +6 -0
  17. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -2
  18. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -11
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -63
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/cockpit.css +3 -3
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +40 -40
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx +17 -17
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx +32 -32
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx +7 -7
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx +9 -9
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +40 -40
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar-locales.ts +3 -3
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -14
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx +6 -6
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx +6 -6
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx +6 -6
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css +1 -1
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -3
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -58
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -1
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -5
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -4
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -106
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +235 -235
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -20
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -5
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/retail.css +12 -12
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/presentation.ts +36 -36
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/table-data.ts +47 -47
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -18
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/widget-queries.ts +18 -18
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/fixtures.mjs +28 -28
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/index.html +16 -16
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/main.tsx +149 -149
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/test-page.css +4 -4
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/browser/vite.config.mjs +13 -13
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/bundle.test.mjs +19 -19
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/cockpit.test.mjs +73 -73
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/field-selection.test.mjs +70 -70
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/retail.test.mjs +31 -31
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -189
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
  58. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  59. package/src/app.js +160 -157
  60. package/src/config.js +27 -27
  61. package/src/help.js +55 -55
  62. package/src/install-skill.js +89 -89
  63. package/src/profiles.js +158 -158
  64. package/src/skill-files.js +95 -94
  65. package/templates/web-app/docs/acceptance.md +80 -80
  66. package/templates/web-app/examples/cockpit.config.json +22 -22
  67. package/templates/web-app/examples/retail.config.json +185 -185
  68. package/templates/web-app/gitignore.template +6 -0
  69. package/templates/web-app/index.html +2 -2
  70. package/templates/web-app/public/app-config.json +11 -11
  71. package/templates/web-app/src/App.tsx +63 -63
  72. package/templates/web-app/src/cockpit.css +3 -3
  73. package/templates/web-app/src/components/Chart.tsx +40 -40
  74. package/templates/web-app/src/components/Insights.tsx +17 -17
  75. package/templates/web-app/src/components/MonthPicker.tsx +32 -32
  76. package/templates/web-app/src/components/OptionFilter.tsx +7 -7
  77. package/templates/web-app/src/components/RangeFilter.tsx +9 -9
  78. package/templates/web-app/src/components/Widget.tsx +40 -40
  79. package/templates/web-app/src/components/calendar-locales.ts +3 -3
  80. package/templates/web-app/src/components/calendar.ts +14 -14
  81. package/templates/web-app/src/components/charts/Bar.tsx +6 -6
  82. package/templates/web-app/src/components/charts/Line.tsx +6 -6
  83. package/templates/web-app/src/components/charts/Pie.tsx +6 -6
  84. package/templates/web-app/src/components/month-picker.css +1 -1
  85. package/templates/web-app/src/finebi/config.d.ts +3 -3
  86. package/templates/web-app/src/finebi/config.js +58 -58
  87. package/templates/web-app/src/finebi/date.d.ts +1 -1
  88. package/templates/web-app/src/finebi/date.js +5 -5
  89. package/templates/web-app/src/finebi/filters.d.ts +4 -4
  90. package/templates/web-app/src/finebi/filters.js +106 -106
  91. package/templates/web-app/src/finebi/normalize.ts +235 -235
  92. package/templates/web-app/src/finebi/types.ts +20 -20
  93. package/templates/web-app/src/main.tsx +5 -5
  94. package/templates/web-app/src/retail.css +12 -12
  95. package/templates/web-app/src/services/presentation.ts +36 -36
  96. package/templates/web-app/src/services/table-data.ts +47 -47
  97. package/templates/web-app/src/services/view-data.ts +18 -18
  98. package/templates/web-app/src/services/widget-queries.ts +18 -18
  99. package/templates/web-app/tests/browser/fixtures.mjs +28 -28
  100. package/templates/web-app/tests/browser/index.html +16 -16
  101. package/templates/web-app/tests/browser/main.tsx +149 -149
  102. package/templates/web-app/tests/browser/test-page.css +4 -4
  103. package/templates/web-app/tests/browser/vite.config.mjs +13 -13
  104. package/templates/web-app/tests/bundle.test.mjs +19 -19
  105. package/templates/web-app/tests/cockpit.test.mjs +73 -73
  106. package/templates/web-app/tests/field-selection.test.mjs +70 -70
  107. package/templates/web-app/tests/retail.test.mjs +31 -31
  108. package/templates/web-app/tests/table-normalize.test.mjs +189 -189
  109. package/templates/web-app/tsconfig.json +4 -4
@@ -1,36 +1,36 @@
1
- import type {Cell,WidgetBinding} from '../finebi/types';
2
- import {displayCell} from './view-data';
3
-
4
- // Decimal strings stay strings/integers: formatting a large amount must not lose cents.
5
- export function formatMetric(cell:Cell,format?:WidgetBinding['valueFormat']):string {
6
- if(!format||cell.state!=='AVAILABLE'||cell.value===null)return displayCell(cell);
7
- const match=/^(-?)(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(cell.value);
8
- if(!match)return displayCell(cell);
9
- const exponent=Number(match[4]??0),fraction=match[3]??'';
10
- if(Math.abs(exponent)>100)return displayCell(cell);
11
- let digits=(match[2]+fraction).replace(/^0+(?=\d)/,'');
12
- let scale=fraction.length-exponent;
13
- if(format==='percent') {
14
- scale-=2;
15
- const shift=2-scale;
16
- let amount=BigInt(digits);
17
- if(shift>=0)amount*=10n**BigInt(shift);
18
- else {const divisor=10n**BigInt(-shift);amount=(amount+divisor/2n)/divisor;}
19
- digits=amount.toString();scale=2;
20
- }
21
- if(scale<0){digits+='0'.repeat(-scale);scale=0;}
22
- digits=digits.padStart(scale+1,'0');
23
- const whole=(scale?digits.slice(0,-scale):digits).replace(/\B(?=(\d{3})+(?!\d))/g,',');
24
- const tail=scale?digits.slice(-scale):'';
25
- const decimal=format==='percent'?tail:tail.replace(/0+$/,'');
26
- const sign=match[1]&&/[1-9]/.test(digits)?'-':'';
27
- return `${sign}${whole}${decimal?'.'+decimal:''}${format==='percent'?'%':''}`;
28
- }
29
-
30
- export function formatCategory(cell:Cell,binding:Pick<WidgetBinding,'categoryFormat'|'categoryTimeZone'>):string {
31
- if(binding.categoryFormat!=='epoch-month'||!binding.categoryTimeZone||cell.state!=='AVAILABLE'||!cell.value||! /^-?\d+$/.test(cell.value))return displayCell(cell);
32
- const timestamp=Number(cell.value);
33
- if(!Number.isSafeInteger(timestamp)||!Number.isFinite(new Date(timestamp).getTime()))return displayCell(cell);
34
- const parts=new Intl.DateTimeFormat('en',{year:'numeric',month:'2-digit',timeZone:binding.categoryTimeZone}).formatToParts(new Date(timestamp));
35
- return `${parts.find(p=>p.type==='year')!.value}-${parts.find(p=>p.type==='month')!.value}`;
36
- }
1
+ import type {Cell,WidgetBinding} from '../finebi/types';
2
+ import {displayCell} from './view-data';
3
+
4
+ // Decimal strings stay strings/integers: formatting a large amount must not lose cents.
5
+ export function formatMetric(cell:Cell,format?:WidgetBinding['valueFormat']):string {
6
+ if(!format||cell.state!=='AVAILABLE'||cell.value===null)return displayCell(cell);
7
+ const match=/^(-?)(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(cell.value);
8
+ if(!match)return displayCell(cell);
9
+ const exponent=Number(match[4]??0),fraction=match[3]??'';
10
+ if(Math.abs(exponent)>100)return displayCell(cell);
11
+ let digits=(match[2]+fraction).replace(/^0+(?=\d)/,'');
12
+ let scale=fraction.length-exponent;
13
+ if(format==='percent') {
14
+ scale-=2;
15
+ const shift=2-scale;
16
+ let amount=BigInt(digits);
17
+ if(shift>=0)amount*=10n**BigInt(shift);
18
+ else {const divisor=10n**BigInt(-shift);amount=(amount+divisor/2n)/divisor;}
19
+ digits=amount.toString();scale=2;
20
+ }
21
+ if(scale<0){digits+='0'.repeat(-scale);scale=0;}
22
+ digits=digits.padStart(scale+1,'0');
23
+ const whole=(scale?digits.slice(0,-scale):digits).replace(/\B(?=(\d{3})+(?!\d))/g,',');
24
+ const tail=scale?digits.slice(-scale):'';
25
+ const decimal=format==='percent'?tail:tail.replace(/0+$/,'');
26
+ const sign=match[1]&&/[1-9]/.test(digits)?'-':'';
27
+ return `${sign}${whole}${decimal?'.'+decimal:''}${format==='percent'?'%':''}`;
28
+ }
29
+
30
+ export function formatCategory(cell:Cell,binding:Pick<WidgetBinding,'categoryFormat'|'categoryTimeZone'>):string {
31
+ if(binding.categoryFormat!=='epoch-month'||!binding.categoryTimeZone||cell.state!=='AVAILABLE'||!cell.value||! /^-?\d+$/.test(cell.value))return displayCell(cell);
32
+ const timestamp=Number(cell.value);
33
+ if(!Number.isSafeInteger(timestamp)||!Number.isFinite(new Date(timestamp).getTime()))return displayCell(cell);
34
+ const parts=new Intl.DateTimeFormat('en',{year:'numeric',month:'2-digit',timeZone:binding.categoryTimeZone}).formatToParts(new Date(timestamp));
35
+ return `${parts.find(p=>p.type==='year')!.value}-${parts.find(p=>p.type==='month')!.value}`;
36
+ }
@@ -1,47 +1,47 @@
1
- import type {Cell,Dataset,WidgetBinding} from '../finebi/types';
2
- import {displayCell} from './view-data';
3
- function decimal(value:string) {
4
- const match=/^([+-]?)(\d*\.?\d+|\d+\.)(?:e([+-]?\d+))?$/i.exec(value);
5
- if(!match)throw new Error('无效十进制值');
6
- const [whole,fraction='']=match[2].split('.');
7
- const digits=(whole+fraction).replace(/^0+/,'').replace(/0+$/,'');
8
- const significant=(whole+fraction).replace(/^0+/,'');
9
- return {sign:!digits?0:match[1]==='-'?-1:1,digits,order:BigInt(significant.length)+BigInt(match[3]||0)-BigInt(fraction.length)};
10
- }
11
- export function compareDecimal(a:string,b:string) {
12
- const x=decimal(a),y=decimal(b);
13
- if(x.sign!==y.sign)return x.sign<y.sign?-1:1;
14
- if(x.sign===0)return 0;
15
- const length=Math.max(x.digits.length,y.digits.length);
16
- const left=x.digits.padEnd(length,'0'),right=y.digits.padEnd(length,'0');
17
- const order=x.order!==y.order?(x.order<y.order?-1:1):left===right?0:left<right?-1:1;
18
- return order===0?0:order*x.sign;
19
- }
20
- export function tableDataset(dataset:Dataset,binding:WidgetBinding):Dataset {
21
- if(!binding.tableFieldIds)return dataset;
22
- const indices=binding.tableFieldIds.map(id=>fieldIndex(dataset,id));
23
- return {...dataset,columns:indices.map(i=>dataset.columns[i]),rows:dataset.rows.map(row=>({...row,cells:indices.map(i=>row.cells[i])}))};
24
- }
25
- export function fieldIndex(dataset:Dataset,id:string) {
26
- const index=dataset.columns.findIndex(c=>c.fieldId===id);
27
- if(index<0)throw new Error(`结果字段绑定缺失:${id}`);
28
- return index;
29
- }
30
- type Options={search:string;sort?:{fieldId:string;direction:'asc'|'desc'};filters:Record<string,string>};
31
- export function tableRows(dataset:Dataset,{search,sort,filters}:Options) {
32
- const columns=Object.entries(filters).map(([id,value])=>({index:fieldIndex(dataset,id),value}));
33
- const rows=dataset.rows.filter(row=>row.cells.some(c=>displayCell(c).toLocaleLowerCase().includes(search.toLocaleLowerCase()))&&columns.every(({index,value})=>!value||displayCell(row.cells[index])===value));
34
- if(!sort)return rows;
35
- const index=fieldIndex(dataset,sort.fieldId),numeric=dataset.columns[index].type==='DECIMAL';
36
- return [...rows].sort((a,b)=>{
37
- const x=a.cells[index],y=b.cells[index];
38
- const valid=(c:Cell)=>c.state==='AVAILABLE'&&c.value!==null;
39
- if(!valid(x)||!valid(y))return valid(x)?-1:valid(y)?1:0;
40
- const order=numeric?compareDecimal(x.value!,y.value!):displayCell(x).localeCompare(displayCell(y),'zh-CN');
41
- return sort.direction==='asc'?order:-order;
42
- });
43
- }
44
- export function tableCsv(dataset:Dataset,rows:Dataset['rows']) {
45
- const escape=(value:string)=>`"${(/^[\s]*[=+\-@]/.test(value)?"'"+value:value).replace(/"/g,'""')}"`;
46
- return '\uFEFF'+[[...dataset.columns.map(c=>c.name),'行类型'],...rows.map(row=>[...row.cells.map(displayCell),row.kind])].map(row=>row.map(escape).join(',')).join('\r\n');
47
- }
1
+ import type {Cell,Dataset,WidgetBinding} from '../finebi/types';
2
+ import {displayCell} from './view-data';
3
+ function decimal(value:string) {
4
+ const match=/^([+-]?)(\d*\.?\d+|\d+\.)(?:e([+-]?\d+))?$/i.exec(value);
5
+ if(!match)throw new Error('无效十进制值');
6
+ const [whole,fraction='']=match[2].split('.');
7
+ const digits=(whole+fraction).replace(/^0+/,'').replace(/0+$/,'');
8
+ const significant=(whole+fraction).replace(/^0+/,'');
9
+ return {sign:!digits?0:match[1]==='-'?-1:1,digits,order:BigInt(significant.length)+BigInt(match[3]||0)-BigInt(fraction.length)};
10
+ }
11
+ export function compareDecimal(a:string,b:string) {
12
+ const x=decimal(a),y=decimal(b);
13
+ if(x.sign!==y.sign)return x.sign<y.sign?-1:1;
14
+ if(x.sign===0)return 0;
15
+ const length=Math.max(x.digits.length,y.digits.length);
16
+ const left=x.digits.padEnd(length,'0'),right=y.digits.padEnd(length,'0');
17
+ const order=x.order!==y.order?(x.order<y.order?-1:1):left===right?0:left<right?-1:1;
18
+ return order===0?0:order*x.sign;
19
+ }
20
+ export function tableDataset(dataset:Dataset,binding:WidgetBinding):Dataset {
21
+ if(!binding.tableFieldIds)return dataset;
22
+ const indices=binding.tableFieldIds.map(id=>fieldIndex(dataset,id));
23
+ return {...dataset,columns:indices.map(i=>dataset.columns[i]),rows:dataset.rows.map(row=>({...row,cells:indices.map(i=>row.cells[i])}))};
24
+ }
25
+ export function fieldIndex(dataset:Dataset,id:string) {
26
+ const index=dataset.columns.findIndex(c=>c.fieldId===id);
27
+ if(index<0)throw new Error(`结果字段绑定缺失:${id}`);
28
+ return index;
29
+ }
30
+ type Options={search:string;sort?:{fieldId:string;direction:'asc'|'desc'};filters:Record<string,string>};
31
+ export function tableRows(dataset:Dataset,{search,sort,filters}:Options) {
32
+ const columns=Object.entries(filters).map(([id,value])=>({index:fieldIndex(dataset,id),value}));
33
+ const rows=dataset.rows.filter(row=>row.cells.some(c=>displayCell(c).toLocaleLowerCase().includes(search.toLocaleLowerCase()))&&columns.every(({index,value})=>!value||displayCell(row.cells[index])===value));
34
+ if(!sort)return rows;
35
+ const index=fieldIndex(dataset,sort.fieldId),numeric=dataset.columns[index].type==='DECIMAL';
36
+ return [...rows].sort((a,b)=>{
37
+ const x=a.cells[index],y=b.cells[index];
38
+ const valid=(c:Cell)=>c.state==='AVAILABLE'&&c.value!==null;
39
+ if(!valid(x)||!valid(y))return valid(x)?-1:valid(y)?1:0;
40
+ const order=numeric?compareDecimal(x.value!,y.value!):displayCell(x).localeCompare(displayCell(y),'zh-CN');
41
+ return sort.direction==='asc'?order:-order;
42
+ });
43
+ }
44
+ export function tableCsv(dataset:Dataset,rows:Dataset['rows']) {
45
+ const escape=(value:string)=>`"${(/^[\s]*[=+\-@]/.test(value)?"'"+value:value).replace(/"/g,'""')}"`;
46
+ return '\uFEFF'+[[...dataset.columns.map(c=>c.name),'行类型'],...rows.map(row=>[...row.cells.map(displayCell),row.kind])].map(row=>row.map(escape).join(',')).join('\r\n');
47
+ }
@@ -1,18 +1,18 @@
1
- import type {Cell, Dataset, QueryResult, WidgetBinding} from '../finebi/types';
2
- export function selectDataset(result: QueryResult, id?: string): Dataset {
3
- if (result.normalization === 'UNSUPPORTED') throw new Error('当前结果类型尚未支持,请使用 FineBI 原生查看页');
4
- const dataset = id ? result.datasets.find(d => d.id === id) : result.datasets.length === 1 ? result.datasets[0] : undefined;
5
- if (!dataset) throw new Error('请绑定唯一的数据模型 datasetId');
6
- return dataset;
7
- }
8
- export const displayCell = (cell: Cell) => cell.state === 'RESTRICTED' ? '受限' : cell.display ?? (cell.state === 'AVAILABLE' ? cell.value : null) ?? (cell.state === 'NULL' ? '—' : '不可用');
9
- export function chartValues(dataset: Dataset, binding: Pick<WidgetBinding, 'categoryFieldId' | 'valueFieldId'>) {
10
- const x = dataset.columns.findIndex(c => c.fieldId === binding.categoryFieldId);
11
- const y = dataset.columns.findIndex(c => c.fieldId === binding.valueFieldId);
12
- if (x < 0 || y < 0 || dataset.columns[y].type !== 'DECIMAL') throw new Error('图表字段绑定缺失或类型不匹配');
13
- return dataset.rows.map(row => {
14
- const cell = row.cells[y];
15
- const n = cell.state === 'AVAILABLE' ? Number(cell.value) : NaN;
16
- return {name: displayCell(row.cells[x]), value: Number.isFinite(n) ? n : null};
17
- });
18
- }
1
+ import type {Cell, Dataset, QueryResult, WidgetBinding} from '../finebi/types';
2
+ export function selectDataset(result: QueryResult, id?: string): Dataset {
3
+ if (result.normalization === 'UNSUPPORTED') throw new Error('当前结果类型尚未支持,请使用 FineBI 原生查看页');
4
+ const dataset = id ? result.datasets.find(d => d.id === id) : result.datasets.length === 1 ? result.datasets[0] : undefined;
5
+ if (!dataset) throw new Error('请绑定唯一的数据模型 datasetId');
6
+ return dataset;
7
+ }
8
+ export const displayCell = (cell: Cell) => cell.state === 'RESTRICTED' ? '受限' : cell.display ?? (cell.state === 'AVAILABLE' ? cell.value : null) ?? (cell.state === 'NULL' ? '—' : '不可用');
9
+ export function chartValues(dataset: Dataset, binding: Pick<WidgetBinding, 'categoryFieldId' | 'valueFieldId'>) {
10
+ const x = dataset.columns.findIndex(c => c.fieldId === binding.categoryFieldId);
11
+ const y = dataset.columns.findIndex(c => c.fieldId === binding.valueFieldId);
12
+ if (x < 0 || y < 0 || dataset.columns[y].type !== 'DECIMAL') throw new Error('图表字段绑定缺失或类型不匹配');
13
+ return dataset.rows.map(row => {
14
+ const cell = row.cells[y];
15
+ const n = cell.state === 'AVAILABLE' ? Number(cell.value) : NaN;
16
+ return {name: displayCell(row.cells[x]), value: Number.isFinite(n) ? n : null};
17
+ });
18
+ }
@@ -1,18 +1,18 @@
1
- import {queryWidget} from '../finebi/client';
2
- import type {AppConfig,FilterState,QueryResult,WidgetBinding} from '../finebi/types';
3
- export type WidgetState={loading:boolean;result?:QueryResult;error?:Error;at?:string};
4
- export const widgetKey=(binding:WidgetBinding,index:number)=>`${index}:${binding.entryId}`;
5
- export async function loadWidgets(config:AppConfig,filters:FilterState,signal:AbortSignal,onState:(id:string,state:WidgetState)=>void,fetchImpl:typeof fetch=fetch) {
6
- const queue=config.widgets.map((binding,index)=>({binding,key:widgetKey(binding,index)}));
7
- const worker=async()=>{
8
- while(queue.length&&!signal.aborted) {
9
- const {binding,key}=queue.shift()!;
10
- onState(key,{loading:true});
11
- try {
12
- const result=await queryWidget(config,binding,filters,signal,fetchImpl);
13
- if(!signal.aborted)onState(key,{loading:false,result,at:new Date().toLocaleTimeString()});
14
- } catch(error) {if(!signal.aborted)onState(key,{loading:false,error:error instanceof Error?error:new Error('查询失败')});}
15
- }
16
- };
17
- await Promise.all(Array.from({length:Math.min(3,queue.length)},worker));
18
- }
1
+ import {queryWidget} from '../finebi/client';
2
+ import type {AppConfig,FilterState,QueryResult,WidgetBinding} from '../finebi/types';
3
+ export type WidgetState={loading:boolean;result?:QueryResult;error?:Error;at?:string};
4
+ export const widgetKey=(binding:WidgetBinding,index:number)=>`${index}:${binding.entryId}`;
5
+ export async function loadWidgets(config:AppConfig,filters:FilterState,signal:AbortSignal,onState:(id:string,state:WidgetState)=>void,fetchImpl:typeof fetch=fetch) {
6
+ const queue=config.widgets.map((binding,index)=>({binding,key:widgetKey(binding,index)}));
7
+ const worker=async()=>{
8
+ while(queue.length&&!signal.aborted) {
9
+ const {binding,key}=queue.shift()!;
10
+ onState(key,{loading:true});
11
+ try {
12
+ const result=await queryWidget(config,binding,filters,signal,fetchImpl);
13
+ if(!signal.aborted)onState(key,{loading:false,result,at:new Date().toLocaleTimeString()});
14
+ } catch(error) {if(!signal.aborted)onState(key,{loading:false,error:error instanceof Error?error:new Error('查询失败')});}
15
+ }
16
+ };
17
+ await Promise.all(Array.from({length:Math.min(3,queue.length)},worker));
18
+ }
@@ -1,28 +1,28 @@
1
- // Synthetic native responses, confined to the browser acceptance page.
2
- export function fixtureConfig(layout, chartCount = 8) {
3
- return {schemaVersion: '1', title: '合成门店经营验证', basePath: '/fixture-decision', layout,
4
- dashboardId: 'fixture-dashboard', directoryEntryId: 'fixture-directory', maxRows: 1000, pageSize: 8,
5
- filters: [{name: '区域', label: '区域', type: 'text'}], widgets: [
6
- ...Array.from({length: 4}, (_, i) => ({entryId: `kpi${i}`, name: i === 3 ? '独立范围指标' : `合成指标 ${i + 1}`,
7
- type: 'kpi', valueFieldId: 'amount', valueFormat: 'number', filterNames: i === 3 ? [] : ['区域']})),
8
- ...Array.from({length: chartCount}, (_, i) => ({entryId: `chart${i}`, name: `${['趋势','对比','构成'][i % 3]} ${i + 1} · 较长标题用于检查文字换行和布局边界`,
9
- type: ['line','bar','pie'][i % 3], categoryFieldId: 'region', valueFieldId: 'amount', filterNames: ['区域']})),
10
- {entryId: 'table', name: '合成明细', type: 'table', filterNames: ['区域']},
11
- ]};
12
- }
13
-
14
- export function nativeFixture(id, refer, scenario) {
15
- const filtered = refer['区域']?.[0] === '华东';
16
- const kpi = id.startsWith('kpi');
17
- const headers = kpi ? [{dId: 'amount', text: '金额'}] : [{dId: 'region', text: '区域'}, {dId: 'amount', text: '金额'}];
18
- const cell = (value, authority = true) => ({value, authority});
19
- let rows = kpi ? [[cell(filtered ? '120' : '260')]]
20
- : (filtered ? [['华东','120']] : [['华东','120'],['华北','80'],['华南','60']]).map(([region, amount]) => [cell(region), cell(amount)]);
21
- if (!filtered && id === 'chart0') rows = [[cell('华东'),cell('120')],[cell('华北'),cell(null)],[cell('华南'),cell('-20')]];
22
- if (!filtered && id === 'table') rows[1][1] = cell('hidden', false);
23
- if (scenario === 'empty') rows = [];
24
- const partial = scenario === 'partial' && id === 'chart0';
25
- if (partial) rows[1] = [cell('华北')];
26
- return {previewResult: {resultType: 'DETAIL', header: headers, items: rows, row: partial ? 30 : rows.length,
27
- size: 1000, hasNext: partial}, widgetBean: {dimensions: {region: {type: 1}, amount: {type: 2}}}};
28
- }
1
+ // Synthetic native responses, confined to the browser acceptance page.
2
+ export function fixtureConfig(layout, chartCount = 8) {
3
+ return {schemaVersion: '1', title: '合成门店经营验证', basePath: '/fixture-decision', layout,
4
+ dashboardId: 'fixture-dashboard', directoryEntryId: 'fixture-directory', maxRows: 1000, pageSize: 8,
5
+ filters: [{name: '区域', label: '区域', type: 'text'}], widgets: [
6
+ ...Array.from({length: 4}, (_, i) => ({entryId: `kpi${i}`, name: i === 3 ? '独立范围指标' : `合成指标 ${i + 1}`,
7
+ type: 'kpi', valueFieldId: 'amount', valueFormat: 'number', filterNames: i === 3 ? [] : ['区域']})),
8
+ ...Array.from({length: chartCount}, (_, i) => ({entryId: `chart${i}`, name: `${['趋势','对比','构成'][i % 3]} ${i + 1} · 较长标题用于检查文字换行和布局边界`,
9
+ type: ['line','bar','pie'][i % 3], categoryFieldId: 'region', valueFieldId: 'amount', filterNames: ['区域']})),
10
+ {entryId: 'table', name: '合成明细', type: 'table', filterNames: ['区域']},
11
+ ]};
12
+ }
13
+
14
+ export function nativeFixture(id, refer, scenario) {
15
+ const filtered = refer['区域']?.[0] === '华东';
16
+ const kpi = id.startsWith('kpi');
17
+ const headers = kpi ? [{dId: 'amount', text: '金额'}] : [{dId: 'region', text: '区域'}, {dId: 'amount', text: '金额'}];
18
+ const cell = (value, authority = true) => ({value, authority});
19
+ let rows = kpi ? [[cell(filtered ? '120' : '260')]]
20
+ : (filtered ? [['华东','120']] : [['华东','120'],['华北','80'],['华南','60']]).map(([region, amount]) => [cell(region), cell(amount)]);
21
+ if (!filtered && id === 'chart0') rows = [[cell('华东'),cell('120')],[cell('华北'),cell(null)],[cell('华南'),cell('-20')]];
22
+ if (!filtered && id === 'table') rows[1][1] = cell('hidden', false);
23
+ if (scenario === 'empty') rows = [];
24
+ const partial = scenario === 'partial' && id === 'chart0';
25
+ if (partial) rows[1] = [cell('华北')];
26
+ return {previewResult: {resultType: 'DETAIL', header: headers, items: rows, row: partial ? 30 : rows.length,
27
+ size: 1000, hasNext: partial}, widgetBean: {dimensions: {region: {type: 1}, amount: {type: 2}}}};
28
+ }
@@ -1,16 +1,16 @@
1
- <!doctype html>
2
- <html lang="zh-CN">
3
- <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>FineBI 模板浏览器验收</title></head>
4
- <body>
5
- <section id="browser-checks" aria-label="浏览器验收">
6
- <h1>模板浏览器验收 · 合成数据</h1>
7
- <p>复用实际页面组件,不连接 FineBI。调整浏览器视口后重新运行;真实身份、业务口径与视觉质量需另外核对。</p>
8
- <label>测试布局 <select id="test-layout"><option>overview</option><option>workbench</option><option>cockpit</option><option selected>retail</option></select></label>
9
- <button id="test-run">运行浏览器检查</button>
10
- <strong id="test-result" role="status">未运行</strong>
11
- <ol id="test-checks"></ol>
12
- </section>
13
- <div id="root"></div>
14
- <script type="module" src="./main.tsx"></script>
15
- </body>
16
- </html>
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>FineBI 模板浏览器验收</title></head>
4
+ <body>
5
+ <section id="browser-checks" aria-label="浏览器验收">
6
+ <h1>模板浏览器验收 · 合成数据</h1>
7
+ <p>复用实际页面组件,不连接 FineBI。调整浏览器视口后重新运行;真实身份、业务口径与视觉质量需另外核对。</p>
8
+ <label>测试布局 <select id="test-layout"><option>overview</option><option>workbench</option><option>cockpit</option><option selected>retail</option></select></label>
9
+ <button id="test-run">运行浏览器检查</button>
10
+ <strong id="test-result" role="status">未运行</strong>
11
+ <ol id="test-checks"></ol>
12
+ </section>
13
+ <div id="root"></div>
14
+ <script type="module" src="./main.tsx"></script>
15
+ </body>
16
+ </html>