@finebi/finebi-cli 1.6.1 → 1.6.2
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 +8 -8
- package/package.json +1 -1
- package/skills/finebi-cli-agent/SKILL.md +43 -123
- package/skills/finebi-cli-agent/agents/openai.yaml +1 -1
- package/skills/finebi-cli-agent/references/artifact-ordering.md +3 -3
- package/skills/finebi-cli-agent/references/command-workflows.md +30 -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/references/dsl-handoff-contract.md +1 -1
- package/skills/finebi-cli-agent/references/handoff-contract.md +6 -4
- package/skills/finebi-cli-agent/references/mutation-confirmation-contract.md +2 -0
- package/skills/finebi-cli-agent/references/scenario-workflows.md +10 -8
- package/skills/finebi-cli-agent/references/web-app.md +4 -2
- package/skills/finebi-cli-agent/references/widget-dsl-core.md +6 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +157 -148
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/browser-auth.js +3 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +28 -28
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/error-catalog.js +2 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +63 -60
- 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 +94 -94
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +18 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +12 -5
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/acceptance.md +80 -67
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -2
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +66 -38
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +2 -2
- 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/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/scripts/browser-session.mjs +59 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +25 -24
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-session.mjs +57 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/session-file.mjs +45 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +63 -56
- 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/DataTable.tsx +1 -1
- 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 -39
- 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/client.ts +20 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.d.ts +3 -1
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/config.js +58 -44
- 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 -77
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +20 -18
- 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 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +20 -5
- 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/runtime.test.mjs +7 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/session.test.mjs +91 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/table-normalize.test.mjs +189 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/vite-auth.test.mjs +41 -0
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +9 -4
- package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
- package/src/app.js +157 -148
- package/src/browser-auth.js +3 -4
- package/src/config.js +28 -28
- package/src/error-catalog.js +2 -2
- package/src/help.js +63 -60
- package/src/install-skill.js +89 -89
- package/src/profiles.js +158 -158
- package/src/skill-files.js +94 -94
- package/templates/web-app/AGENTS.md +18 -5
- package/templates/web-app/README.md +12 -5
- package/templates/web-app/docs/acceptance.md +80 -67
- package/templates/web-app/docs/auth-and-deploy.md +17 -2
- package/templates/web-app/docs/data-contract.md +66 -38
- package/templates/web-app/docs/filters.md +2 -2
- package/templates/web-app/examples/cockpit.config.json +22 -22
- package/templates/web-app/examples/retail.config.json +185 -185
- package/templates/web-app/index.html +2 -2
- package/templates/web-app/public/app-config.json +11 -11
- package/templates/web-app/scripts/browser-session.mjs +59 -0
- package/templates/web-app/scripts/dev-proxy.mjs +25 -24
- package/templates/web-app/scripts/dev-session.mjs +57 -0
- package/templates/web-app/scripts/session-file.mjs +45 -0
- package/templates/web-app/src/App.tsx +63 -56
- 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/DataTable.tsx +1 -1
- 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 -39
- 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/client.ts +20 -4
- package/templates/web-app/src/finebi/config.d.ts +3 -1
- package/templates/web-app/src/finebi/config.js +58 -44
- 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 -77
- package/templates/web-app/src/finebi/types.ts +20 -18
- 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 -0
- package/templates/web-app/tests/proxy.test.mjs +20 -5
- package/templates/web-app/tests/retail.test.mjs +31 -31
- package/templates/web-app/tests/runtime.test.mjs +7 -0
- package/templates/web-app/tests/session.test.mjs +91 -0
- package/templates/web-app/tests/table-normalize.test.mjs +189 -0
- package/templates/web-app/tests/vite-auth.test.mjs +41 -0
- package/templates/web-app/tsconfig.json +4 -4
- package/templates/web-app/vite.config.ts +9 -4
|
@@ -1,56 +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
|
|
14
|
-
useEffect(()=>{
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
setStates(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
};
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
{retail&&<
|
|
46
|
-
<div className="
|
|
47
|
-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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}}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import {useEffect, useRef, useState} from 'react';
|
|
2
|
-
import * as echarts from 'echarts/core';
|
|
3
|
-
import {TooltipComponent} from 'echarts/components';
|
|
4
|
-
import {CanvasRenderer} from 'echarts/renderers';
|
|
5
|
-
import type {Dataset, WidgetBinding} from '../finebi/types';
|
|
6
|
-
import {chartValues,displayCell} from '../services/view-data';
|
|
7
|
-
import {formatCategory} from '../services/presentation';
|
|
8
|
-
echarts.use([TooltipComponent, CanvasRenderer]);
|
|
9
|
-
export function Chart({dataset, binding}: {dataset: Dataset; binding: WidgetBinding}) {
|
|
10
|
-
const container = useRef<HTMLDivElement>(null);
|
|
11
|
-
const [order, setOrder] = useState('source');
|
|
12
|
-
const [metric,setMetric]=useState(0),[hidden,setHidden]=useState<string[]>([]);
|
|
13
|
-
const series=binding.series??[{fieldId:binding.valueFieldId!,label:binding.name}];
|
|
14
|
-
const all=series.map(s=>({...s,values:chartValues(dataset,{categoryFieldId:binding.categoryFieldId,valueFieldId:s.fieldId})}));
|
|
15
|
-
const active=binding.type==='bar'?[all[metric]??all[0]]:all.filter(s=>!hidden.includes(s.fieldId));
|
|
16
|
-
const values=all[0].values;
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (!container.current) return;
|
|
19
|
-
const chart = echarts.init(container.current);
|
|
20
|
-
const style=getComputedStyle(container.current);
|
|
21
|
-
const palette=['#197a58','#4988bd','#d6aa4e','#8b75b5','#ec4899'].map((color,i)=>style.getPropertyValue(`--chart-${i+1}`).trim()||color);
|
|
22
|
-
const categoryIndex=dataset.columns.findIndex(c=>c.fieldId===binding.categoryFieldId);
|
|
23
|
-
const categoryNames=dataset.rows.map(row=>formatCategory(row.cells[categoryIndex],binding));
|
|
24
|
-
const indices=values.map((_,i)=>i),ordered=active[0]?.values??values;
|
|
25
|
-
if(order!=='source')indices.sort((i,j)=>ordered[i].value===null?1:ordered[j].value===null?-1:(ordered[i].value!-ordered[j].value!)*(order==='asc'?1:-1));
|
|
26
|
-
chart.setOption({
|
|
27
|
-
color: palette,
|
|
28
|
-
tooltip: {trigger: binding.type === 'pie' ? 'item' : 'axis', renderMode: 'richText'},
|
|
29
|
-
...(binding.type === 'pie' ? {legend:{bottom:0}} : {
|
|
30
|
-
grid:{left:24,right:24,top:24,bottom:24,containLabel:true},
|
|
31
|
-
xAxis:{type:'category',data:indices.map(i=>categoryNames[i]),axisLabel:{rotate:values.length>8?30:0}},
|
|
32
|
-
yAxis:{type:'value'},
|
|
33
|
-
}),
|
|
34
|
-
series: active.map(s=>({name:s.label,type:binding.type,connectNulls:false,data:indices.map(i=>binding.type==='pie'?s.values[i]:s.values[i].value),radius:binding.type==='pie'?['42%','67%']:undefined})),
|
|
35
|
-
});
|
|
36
|
-
const observer = new ResizeObserver(() => chart.resize()); observer.observe(container.current);
|
|
37
|
-
return () => {observer.disconnect(); chart.dispose();};
|
|
38
|
-
}, [dataset, binding, order,metric,hidden]);
|
|
39
|
-
return <>{all.length>1&&<div className="series-controls">{binding.type==='bar'?<label>显示指标 <select aria-label={`${binding.name}指标`} value={metric} onChange={e=>setMetric(Number(e.target.value))}>{all.map((s,i)=><option key={s.fieldId} value={i}>{s.label}</option>)}</select></label>:all.map(s=><label key={s.fieldId}><input type="checkbox" checked={!hidden.includes(s.fieldId)} onChange={e=>setHidden(old=>e.target.checked?old.filter(id=>id!==s.fieldId):[...old,s.fieldId])}/>{s.label}</label>)}</div>}<label className="chart-sort">显示顺序 <select aria-label={`${binding.name}显示顺序`} value={order} onChange={e => setOrder(e.target.value)}><option value="source">查询顺序</option><option value="desc">已返回值降序</option><option value="asc">已返回值升序</option></select></label><div className="chart" ref={container} role="img" aria-label={binding.name}/><details><summary>查看图表原始显示值(已加载结果)</summary><div className="table-scroll"><table><thead><tr>{dataset.columns.map(c=><th key={c.key}>{c.name}</th>)}</tr></thead><tbody>{dataset.rows.map((row,i)=><tr key={i}>{row.cells.map((cell,j)=><td key={j}>{displayCell(cell)}</td>)}</tr>)}</tbody></table></div></details></>;
|
|
40
|
-
}
|
|
1
|
+
import {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import * as echarts from 'echarts/core';
|
|
3
|
+
import {TooltipComponent} from 'echarts/components';
|
|
4
|
+
import {CanvasRenderer} from 'echarts/renderers';
|
|
5
|
+
import type {Dataset, WidgetBinding} from '../finebi/types';
|
|
6
|
+
import {chartValues,displayCell} from '../services/view-data';
|
|
7
|
+
import {formatCategory} from '../services/presentation';
|
|
8
|
+
echarts.use([TooltipComponent, CanvasRenderer]);
|
|
9
|
+
export function Chart({dataset, binding}: {dataset: Dataset; binding: WidgetBinding}) {
|
|
10
|
+
const container = useRef<HTMLDivElement>(null);
|
|
11
|
+
const [order, setOrder] = useState('source');
|
|
12
|
+
const [metric,setMetric]=useState(0),[hidden,setHidden]=useState<string[]>([]);
|
|
13
|
+
const series=binding.series??[{fieldId:binding.valueFieldId!,label:binding.name}];
|
|
14
|
+
const all=series.map(s=>({...s,values:chartValues(dataset,{categoryFieldId:binding.categoryFieldId,valueFieldId:s.fieldId})}));
|
|
15
|
+
const active=binding.type==='bar'?[all[metric]??all[0]]:all.filter(s=>!hidden.includes(s.fieldId));
|
|
16
|
+
const values=all[0].values;
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!container.current) return;
|
|
19
|
+
const chart = echarts.init(container.current);
|
|
20
|
+
const style=getComputedStyle(container.current);
|
|
21
|
+
const palette=['#197a58','#4988bd','#d6aa4e','#8b75b5','#ec4899'].map((color,i)=>style.getPropertyValue(`--chart-${i+1}`).trim()||color);
|
|
22
|
+
const categoryIndex=dataset.columns.findIndex(c=>c.fieldId===binding.categoryFieldId);
|
|
23
|
+
const categoryNames=dataset.rows.map(row=>formatCategory(row.cells[categoryIndex],binding));
|
|
24
|
+
const indices=values.map((_,i)=>i),ordered=active[0]?.values??values;
|
|
25
|
+
if(order!=='source')indices.sort((i,j)=>ordered[i].value===null?1:ordered[j].value===null?-1:(ordered[i].value!-ordered[j].value!)*(order==='asc'?1:-1));
|
|
26
|
+
chart.setOption({
|
|
27
|
+
color: palette,
|
|
28
|
+
tooltip: {trigger: binding.type === 'pie' ? 'item' : 'axis', renderMode: 'richText'},
|
|
29
|
+
...(binding.type === 'pie' ? {legend:{bottom:0}} : {
|
|
30
|
+
grid:{left:24,right:24,top:24,bottom:24,containLabel:true},
|
|
31
|
+
xAxis:{type:'category',data:indices.map(i=>categoryNames[i]),axisLabel:{rotate:values.length>8?30:0}},
|
|
32
|
+
yAxis:{type:'value'},
|
|
33
|
+
}),
|
|
34
|
+
series: active.map(s=>({name:s.label,type:binding.type,connectNulls:false,data:indices.map(i=>binding.type==='pie'?s.values[i]:s.values[i].value),radius:binding.type==='pie'?['42%','67%']:undefined})),
|
|
35
|
+
});
|
|
36
|
+
const observer = new ResizeObserver(() => chart.resize()); observer.observe(container.current);
|
|
37
|
+
return () => {observer.disconnect(); chart.dispose();};
|
|
38
|
+
}, [dataset, binding, order,metric,hidden]);
|
|
39
|
+
return <>{all.length>1&&<div className="series-controls">{binding.type==='bar'?<label>显示指标 <select aria-label={`${binding.name}指标`} value={metric} onChange={e=>setMetric(Number(e.target.value))}>{all.map((s,i)=><option key={s.fieldId} value={i}>{s.label}</option>)}</select></label>:all.map(s=><label key={s.fieldId}><input type="checkbox" checked={!hidden.includes(s.fieldId)} onChange={e=>setHidden(old=>e.target.checked?old.filter(id=>id!==s.fieldId):[...old,s.fieldId])}/>{s.label}</label>)}</div>}<label className="chart-sort">显示顺序 <select aria-label={`${binding.name}显示顺序`} value={order} onChange={e => setOrder(e.target.value)}><option value="source">查询顺序</option><option value="desc">已返回值降序</option><option value="asc">已返回值升序</option></select></label><div className="chart" ref={container} role="img" aria-label={binding.name}/><details><summary>查看图表原始显示值(已加载结果)</summary><div className="table-scroll"><table><thead><tr>{dataset.columns.map(c=><th key={c.key}>{c.name}</th>)}</tr></thead><tbody>{dataset.rows.map((row,i)=><tr key={i}>{row.cells.map((cell,j)=><td key={j}>{displayCell(cell)}</td>)}</tr>)}</tbody></table></div></details></>;
|
|
40
|
+
}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx
CHANGED
|
@@ -15,7 +15,7 @@ export function DataTable({dataset, pageSize,binding}: {dataset: Dataset; pageSi
|
|
|
15
15
|
const download=()=>{const url=URL.createObjectURL(new Blob([tableCsv(visible,shown)],{type:'text/csv;charset=utf-8'}));const link=document.createElement('a');link.href=url;link.download='loaded-results.csv';link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
|
|
16
16
|
return <><div className="table-tools"><label>搜索已加载数据 <input placeholder="搜索已加载数据" value={search} onChange={e => {setSearch(e.target.value);setPage(0);}}/></label><span>已加载 {dataset.returnedRowCount} 行 · 匹配 {rows.length} 行</span><button onClick={download}>导出匹配结果 CSV</button></div>
|
|
17
17
|
{(binding.localFilterFieldIds?.length??0)>0&&<div className="local-filters">{binding.localFilterFieldIds!.map(id=>{const index=fieldIndex(dataset,id);return <label key={id}>{dataset.columns[index].name}(已加载值)<select value={filters[id]??''} onChange={e=>{setFilters(old=>({...old,[id]:e.target.value}));setPage(0);}}><option value="">全部已加载值</option>{[...new Set(dataset.rows.map(row=>displayCell(row.cells[index])))].filter(Boolean).map(value=><option key={value}>{value}</option>)}</select></label>;})}</div>}
|
|
18
|
-
<div className="table-scroll"><table><thead><tr><th>序号</th>{visible.columns.map(c => <th key={c.key} aria-sort={sort?.fieldId===c.fieldId?(sort.direction==='asc'?'ascending':'descending'):'none'}><button className="sort-column" onClick={()=>{setSort({fieldId:c.fieldId,direction:sort?.fieldId===c.fieldId&&sort.direction==='asc'?'desc':'asc'});setPage(0);}}>{c.name} {sort?.fieldId===c.fieldId?(sort.direction==='asc'?'↑':'↓'):'↕'}</button></th>)}<th>行类型</th></tr></thead><tbody>{shown.slice(current * pageSize, (current + 1) * pageSize).map((row,i) => <tr key={current * pageSize + i}><td>{current * pageSize + i + 1}</td>{row.cells.map((c,j) => <td key={visible.columns[j].key}>{displayCell(c)}</td>)}<td>{
|
|
18
|
+
<div className="table-scroll"><table><thead><tr><th>序号</th>{visible.columns.map(c => <th key={c.key} aria-sort={sort?.fieldId===c.fieldId?(sort.direction==='asc'?'ascending':'descending'):'none'}><button className="sort-column" onClick={()=>{setSort({fieldId:c.fieldId,direction:sort?.fieldId===c.fieldId&&sort.direction==='asc'?'desc':'asc'});setPage(0);}}>{c.name} {sort?.fieldId===c.fieldId?(sort.direction==='asc'?'↑':'↓'):'↕'}</button></th>)}<th>行类型</th></tr></thead><tbody>{shown.slice(current * pageSize, (current + 1) * pageSize).map((row,i) => <tr key={current * pageSize + i}><td>{current * pageSize + i + 1}</td>{row.cells.map((c,j) => <td key={visible.columns[j].key}>{displayCell(c)}</td>)}<td>{{DATA:'数据',SUBTOTAL:'小计',GRAND_TOTAL:'总计',UNKNOWN:'UNKNOWN'}[row.kind]}</td></tr>)}</tbody></table></div>
|
|
19
19
|
{!rows.length && <p className="empty">没有匹配的数据</p>}
|
|
20
20
|
<nav className="pager" aria-label="已加载数据分页"><button disabled={current===0} onClick={()=>setPage(current-1)}>上一页</button><span>{current+1} / {pages}</span><button disabled={current===pages-1} onClick={()=>setPage(current+1)}>下一页</button></nav></>;
|
|
21
21
|
}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Insights.tsx
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type {AppConfig} from '../finebi/types';
|
|
2
|
-
import type {WidgetState} from '../services/widget-queries';
|
|
3
|
-
import {widgetKey} from '../services/widget-queries';
|
|
4
|
-
import {displayCell,selectDataset} from '../services/view-data';
|
|
5
|
-
export function Insights({config,states}:{config:AppConfig;states:Record<string,WidgetState>}) {
|
|
6
|
-
const notes=config.widgets.map((binding,index)=>{
|
|
7
|
-
const state=states[widgetKey(binding,index)];
|
|
8
|
-
if(!state||state.loading)return `${binding.name}:等待当前查询`;
|
|
9
|
-
if(state.error)return `${binding.name}:查询失败,未参与观察`;
|
|
10
|
-
try {
|
|
11
|
-
const result=state.result!,data=selectDataset(result,binding.datasetId);
|
|
12
|
-
if(binding.type==='kpi'&&data.rows.length===1){const i=data.columns.findIndex(c=>c.fieldId===binding.valueFieldId);if(i<0)throw Error();return `${binding.name}:${displayCell(data.rows[0].cells[i])}(${result.normalization})`;}
|
|
13
|
-
return `${binding.name}:已返回 ${data.returnedRowCount} 行,${result.normalization}${result.range.hasNext?',还有后续数据':''}${data.rows.some(r=>r.kind==='UNKNOWN')?';含 UNKNOWN 行,不自动汇总':''}`;
|
|
14
|
-
} catch{return `${binding.name}:绑定或适配不可用,未参与观察`;}
|
|
15
|
-
});
|
|
16
|
-
return <section className="insight-panel" id="observations"><div><span className="eyebrow">CURRENT QUERY</span><h2>经营观察</h2><p>根据当前组件返回动态生成,范围以各组件提示为准。</p></div><ul>{notes.map((note,i)=><li key={i}>{note}</li>)}</ul></section>;
|
|
17
|
-
}
|
|
1
|
+
import type {AppConfig} from '../finebi/types';
|
|
2
|
+
import type {WidgetState} from '../services/widget-queries';
|
|
3
|
+
import {widgetKey} from '../services/widget-queries';
|
|
4
|
+
import {displayCell,selectDataset} from '../services/view-data';
|
|
5
|
+
export function Insights({config,states}:{config:AppConfig;states:Record<string,WidgetState>}) {
|
|
6
|
+
const notes=config.widgets.map((binding,index)=>{
|
|
7
|
+
const state=states[widgetKey(binding,index)];
|
|
8
|
+
if(!state||state.loading)return `${binding.name}:等待当前查询`;
|
|
9
|
+
if(state.error)return `${binding.name}:查询失败,未参与观察`;
|
|
10
|
+
try {
|
|
11
|
+
const result=state.result!,data=selectDataset(result,binding.datasetId);
|
|
12
|
+
if(binding.type==='kpi'&&data.rows.length===1){const i=data.columns.findIndex(c=>c.fieldId===binding.valueFieldId);if(i<0)throw Error();return `${binding.name}:${displayCell(data.rows[0].cells[i])}(${result.normalization})`;}
|
|
13
|
+
return `${binding.name}:已返回 ${data.returnedRowCount} 行,${result.normalization}${result.range.hasNext?',还有后续数据':''}${data.rows.some(r=>r.kind==='UNKNOWN')?';含 UNKNOWN 行,不自动汇总':''}`;
|
|
14
|
+
} catch{return `${binding.name}:绑定或适配不可用,未参与观察`;}
|
|
15
|
+
});
|
|
16
|
+
return <section className="insight-panel" id="observations"><div><span className="eyebrow">CURRENT QUERY</span><h2>经营观察</h2><p>根据当前组件返回动态生成,范围以各组件提示为准。</p></div><ul>{notes.map((note,i)=><li key={i}>{note}</li>)}</ul></section>;
|
|
17
|
+
}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/MonthPicker.tsx
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import {useId,useLayoutEffect,useRef,useState} from 'react';
|
|
2
|
-
import './month-picker.css';
|
|
3
|
-
const clamp=(n:number,min:number,max:number)=>Math.max(min,Math.min(max,n));
|
|
4
|
-
const pad=(n:number)=>String(n).padStart(4,'0');
|
|
5
|
-
export function MonthPicker({id,label,value,onChange}:{id:string;label:string;value:string;onChange:(value:string)=>void}) {
|
|
6
|
-
const popupId=useId(),root=useRef<HTMLDivElement>(null),trigger=useRef<HTMLButtonElement>(null),grid=useRef<HTMLDivElement>(null);
|
|
7
|
-
const now=new Date(),initialYear=value?Number(value.slice(0,4)):now.getFullYear();
|
|
8
|
-
const [open,setOpen]=useState(false),[mode,setMode]=useState<'months'|'years'>('months');
|
|
9
|
-
const [year,setYear]=useState(initialYear),[cursor,setCursor]=useState(0),[focusTick,setFocusTick]=useState(0);
|
|
10
|
-
const base=clamp(Math.floor((year-1)/12)*12+1,1,9988);
|
|
11
|
-
const focusGrid=()=>setFocusTick(n=>n+1);
|
|
12
|
-
useLayoutEffect(()=>{if(open&&focusTick)grid.current?.querySelector<HTMLButtonElement>(`[data-cell="${cursor}"]`)?.focus();},[focusTick,open,cursor,mode,year]);
|
|
13
|
-
const close=(restore=false)=>{setOpen(false);if(restore)trigger.current?.focus();};
|
|
14
|
-
const show=(focus=false)=>{setYear(initialYear);setMode('months');setCursor(value?Number(value.slice(5))-1:now.getMonth());setOpen(true);if(focus)focusGrid();};
|
|
15
|
-
const choose=(next:string)=>{onChange(next);close(true);};
|
|
16
|
-
const navigate=(delta:number)=>{setYear(y=>clamp(y+delta*(mode==='months'?1:12),1,9999));};
|
|
17
|
-
const keyDown=(e:React.KeyboardEvent)=>{
|
|
18
|
-
if(e.key==='Escape'){e.preventDefault();e.stopPropagation();close(true);return;}
|
|
19
|
-
const delta={ArrowLeft:-1,ArrowRight:1,ArrowUp:-3,ArrowDown:3}[e.key];
|
|
20
|
-
if(delta!==undefined){e.preventDefault();setCursor(c=>clamp(c+delta,0,11));focusGrid();}
|
|
21
|
-
else if(e.key==='Home'||e.key==='End'){e.preventDefault();setCursor(e.key==='Home'?0:11);focusGrid();}
|
|
22
|
-
else if(e.key==='PageUp'||e.key==='PageDown'){e.preventDefault();navigate(e.key==='PageUp'?-1:1);focusGrid();}
|
|
23
|
-
};
|
|
24
|
-
return <div ref={root} className="month-picker" onBlur={e=>{if(e.relatedTarget&&!e.currentTarget.contains(e.relatedTarget))close();}}>
|
|
25
|
-
<button id={id} ref={trigger} type="button" className={`month-trigger ${value?'has-value':''}`} aria-label={label} aria-haspopup="dialog" aria-expanded={open} aria-controls={popupId} onClick={()=>open?close():show()} onKeyDown={e=>{if(e.key==='ArrowDown'){e.preventDefault();show(true);}}}><span>{value?`${value.slice(0,4)}年${value.slice(5)}月`:'源看板默认'}</span><span aria-hidden="true">▦</span></button>
|
|
26
|
-
{open&&<><div className="month-dismiss" onPointerDown={()=>close()}/><div className="month-popover" role="dialog" aria-label={`${label}选择面板`} id={popupId} onKeyDown={e=>{if(e.key==='Escape'){e.preventDefault();e.stopPropagation();close(true);}}}>
|
|
27
|
-
<div className="month-head"><button type="button" aria-label={mode==='months'?'上一年':'上一组年份'} disabled={year===1} onClick={()=>navigate(-1)}>‹</button><button type="button" className="month-year" aria-label={mode==='months'?'选择年份':'返回月份'} onClick={()=>{setMode(mode==='months'?'years':'months');setCursor(mode==='months'?year-base:0);focusGrid();}}>{mode==='months'?`${pad(year)}年`:`${pad(base)} — ${pad(base+11)}`}</button><button type="button" aria-label={mode==='months'?'下一年':'下一组年份'} disabled={year===9999} onClick={()=>navigate(1)}>›</button></div>
|
|
28
|
-
<div ref={grid} className="month-grid" role="grid" aria-label={mode==='months'?'月份':'年份'} onKeyDown={keyDown}>{Array.from({length:4},(_,row)=><div role="row" key={row}>{Array.from({length:3},(_,column)=>{const i=row*3+column,selected=mode==='months'?value===`${pad(year)}-${String(i+1).padStart(2,'0')}`:Number(value.slice(0,4))===base+i;return <button key={i} type="button" role="gridcell" data-cell={i} tabIndex={cursor===i?0:-1} aria-selected={selected} className={selected?'selected':''} onFocus={()=>setCursor(i)} onClick={()=>{if(mode==='months')choose(`${pad(year)}-${String(i+1).padStart(2,'0')}`);else{setYear(base+i);setMode('months');setCursor(value?Number(value.slice(5))-1:0);focusGrid();}}}>{mode==='months'?`${i+1}月`:pad(base+i)}</button>;})}</div>)}</div>
|
|
29
|
-
<div className="month-actions"><button type="button" onClick={()=>choose('')}>默认条件</button><button type="button" onClick={()=>choose(`${pad(now.getFullYear())}-${String(now.getMonth()+1).padStart(2,'0')}`)}>本月</button><button type="button" onClick={()=>choose('')}>清除</button></div><p>选择后点击「应用筛选」取数</p>
|
|
30
|
-
</div></>}
|
|
31
|
-
</div>;
|
|
32
|
-
}
|
|
1
|
+
import {useId,useLayoutEffect,useRef,useState} from 'react';
|
|
2
|
+
import './month-picker.css';
|
|
3
|
+
const clamp=(n:number,min:number,max:number)=>Math.max(min,Math.min(max,n));
|
|
4
|
+
const pad=(n:number)=>String(n).padStart(4,'0');
|
|
5
|
+
export function MonthPicker({id,label,value,onChange}:{id:string;label:string;value:string;onChange:(value:string)=>void}) {
|
|
6
|
+
const popupId=useId(),root=useRef<HTMLDivElement>(null),trigger=useRef<HTMLButtonElement>(null),grid=useRef<HTMLDivElement>(null);
|
|
7
|
+
const now=new Date(),initialYear=value?Number(value.slice(0,4)):now.getFullYear();
|
|
8
|
+
const [open,setOpen]=useState(false),[mode,setMode]=useState<'months'|'years'>('months');
|
|
9
|
+
const [year,setYear]=useState(initialYear),[cursor,setCursor]=useState(0),[focusTick,setFocusTick]=useState(0);
|
|
10
|
+
const base=clamp(Math.floor((year-1)/12)*12+1,1,9988);
|
|
11
|
+
const focusGrid=()=>setFocusTick(n=>n+1);
|
|
12
|
+
useLayoutEffect(()=>{if(open&&focusTick)grid.current?.querySelector<HTMLButtonElement>(`[data-cell="${cursor}"]`)?.focus();},[focusTick,open,cursor,mode,year]);
|
|
13
|
+
const close=(restore=false)=>{setOpen(false);if(restore)trigger.current?.focus();};
|
|
14
|
+
const show=(focus=false)=>{setYear(initialYear);setMode('months');setCursor(value?Number(value.slice(5))-1:now.getMonth());setOpen(true);if(focus)focusGrid();};
|
|
15
|
+
const choose=(next:string)=>{onChange(next);close(true);};
|
|
16
|
+
const navigate=(delta:number)=>{setYear(y=>clamp(y+delta*(mode==='months'?1:12),1,9999));};
|
|
17
|
+
const keyDown=(e:React.KeyboardEvent)=>{
|
|
18
|
+
if(e.key==='Escape'){e.preventDefault();e.stopPropagation();close(true);return;}
|
|
19
|
+
const delta={ArrowLeft:-1,ArrowRight:1,ArrowUp:-3,ArrowDown:3}[e.key];
|
|
20
|
+
if(delta!==undefined){e.preventDefault();setCursor(c=>clamp(c+delta,0,11));focusGrid();}
|
|
21
|
+
else if(e.key==='Home'||e.key==='End'){e.preventDefault();setCursor(e.key==='Home'?0:11);focusGrid();}
|
|
22
|
+
else if(e.key==='PageUp'||e.key==='PageDown'){e.preventDefault();navigate(e.key==='PageUp'?-1:1);focusGrid();}
|
|
23
|
+
};
|
|
24
|
+
return <div ref={root} className="month-picker" onBlur={e=>{if(e.relatedTarget&&!e.currentTarget.contains(e.relatedTarget))close();}}>
|
|
25
|
+
<button id={id} ref={trigger} type="button" className={`month-trigger ${value?'has-value':''}`} aria-label={label} aria-haspopup="dialog" aria-expanded={open} aria-controls={popupId} onClick={()=>open?close():show()} onKeyDown={e=>{if(e.key==='ArrowDown'){e.preventDefault();show(true);}}}><span>{value?`${value.slice(0,4)}年${value.slice(5)}月`:'源看板默认'}</span><span aria-hidden="true">▦</span></button>
|
|
26
|
+
{open&&<><div className="month-dismiss" onPointerDown={()=>close()}/><div className="month-popover" role="dialog" aria-label={`${label}选择面板`} id={popupId} onKeyDown={e=>{if(e.key==='Escape'){e.preventDefault();e.stopPropagation();close(true);}}}>
|
|
27
|
+
<div className="month-head"><button type="button" aria-label={mode==='months'?'上一年':'上一组年份'} disabled={year===1} onClick={()=>navigate(-1)}>‹</button><button type="button" className="month-year" aria-label={mode==='months'?'选择年份':'返回月份'} onClick={()=>{setMode(mode==='months'?'years':'months');setCursor(mode==='months'?year-base:0);focusGrid();}}>{mode==='months'?`${pad(year)}年`:`${pad(base)} — ${pad(base+11)}`}</button><button type="button" aria-label={mode==='months'?'下一年':'下一组年份'} disabled={year===9999} onClick={()=>navigate(1)}>›</button></div>
|
|
28
|
+
<div ref={grid} className="month-grid" role="grid" aria-label={mode==='months'?'月份':'年份'} onKeyDown={keyDown}>{Array.from({length:4},(_,row)=><div role="row" key={row}>{Array.from({length:3},(_,column)=>{const i=row*3+column,selected=mode==='months'?value===`${pad(year)}-${String(i+1).padStart(2,'0')}`:Number(value.slice(0,4))===base+i;return <button key={i} type="button" role="gridcell" data-cell={i} tabIndex={cursor===i?0:-1} aria-selected={selected} className={selected?'selected':''} onFocus={()=>setCursor(i)} onClick={()=>{if(mode==='months')choose(`${pad(year)}-${String(i+1).padStart(2,'0')}`);else{setYear(base+i);setMode('months');setCursor(value?Number(value.slice(5))-1:0);focusGrid();}}}>{mode==='months'?`${i+1}月`:pad(base+i)}</button>;})}</div>)}</div>
|
|
29
|
+
<div className="month-actions"><button type="button" onClick={()=>choose('')}>默认条件</button><button type="button" onClick={()=>choose(`${pad(now.getFullYear())}-${String(now.getMonth()+1).padStart(2,'0')}`)}>本月</button><button type="button" onClick={()=>choose('')}>清除</button></div><p>选择后点击「应用筛选」取数</p>
|
|
30
|
+
</div></>}
|
|
31
|
+
</div>;
|
|
32
|
+
}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/OptionFilter.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import Select from 'antd/es/select';
|
|
2
|
-
import type {FilterBinding,FilterValue} from '../finebi/types';
|
|
3
|
-
|
|
4
|
-
export default function OptionFilter({id,binding,value,onChange}:{id:string;binding:FilterBinding;value:FilterValue;onChange:(value:FilterValue)=>void}) {
|
|
5
|
-
const type=binding.type??'select',text=typeof value==='string'?value:'';
|
|
6
|
-
return <Select id={id} aria-label={binding.label} size="large" className="filter-select" mode={type==='multi-select'?'multiple':undefined} allowClear showSearch={{optionFilterProp:'label'}} maxTagCount="responsive" placeholder={`选择${binding.label},支持搜索`} options={binding.options} disabled={value===null} value={type==='multi-select'?(Array.isArray(value)?value:[]):text||undefined} onChange={selected=>onChange(selected??'')} notFoundContent={`没有匹配的${binding.label}`}/>;
|
|
7
|
-
}
|
|
1
|
+
import Select from 'antd/es/select';
|
|
2
|
+
import type {FilterBinding,FilterValue} from '../finebi/types';
|
|
3
|
+
|
|
4
|
+
export default function OptionFilter({id,binding,value,onChange}:{id:string;binding:FilterBinding;value:FilterValue;onChange:(value:FilterValue)=>void}) {
|
|
5
|
+
const type=binding.type??'select',text=typeof value==='string'?value:'';
|
|
6
|
+
return <Select id={id} aria-label={binding.label} size="large" className="filter-select" mode={type==='multi-select'?'multiple':undefined} allowClear showSearch={{optionFilterProp:'label'}} maxTagCount="responsive" placeholder={`选择${binding.label},支持搜索`} options={binding.options} disabled={value===null} value={type==='multi-select'?(Array.isArray(value)?value:[]):text||undefined} onChange={selected=>onChange(selected??'')} notFoundContent={`没有匹配的${binding.label}`}/>;
|
|
7
|
+
}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/RangeFilter.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import generatePicker from 'antd/es/date-picker/generatePicker';
|
|
2
|
-
import {calendarConfig, calendarValue} from './calendar';
|
|
3
|
-
|
|
4
|
-
const CalendarPicker=generatePicker<Date>(calendarConfig);
|
|
5
|
-
type Interval={start:string;end:string};
|
|
6
|
-
export default function RangeFilter({id,month,value,onChange}:{id:string;month:boolean;value:Interval;onChange:(value:Interval)=>void}) {
|
|
7
|
-
const update=(_dates:unknown,strings:[string,string]|null)=>onChange({start:strings?.[0]||'',end:strings?.[1]||''});
|
|
8
|
-
return <CalendarPicker.RangePicker size="large" className="filter-date-range" id={{start:id+'-start',end:id+'-end'}} picker={month?'month':'date'} format={month?'YYYY-MM':'YYYY-MM-DD'} placeholder={month?['开始月份','结束月份']:['开始日期','结束日期']} allowEmpty={[true,true]} allowClear value={[calendarValue(value.start),calendarValue(value.end)]} minDate={calendarValue('0001-01-01')!} maxDate={calendarValue('9999-12-31')!} onCalendarChange={update} onChange={update} onKeyDown={e=>{if(e.key==='Enter')e.preventDefault();}}/>;
|
|
9
|
-
}
|
|
1
|
+
import generatePicker from 'antd/es/date-picker/generatePicker';
|
|
2
|
+
import {calendarConfig, calendarValue} from './calendar';
|
|
3
|
+
|
|
4
|
+
const CalendarPicker=generatePicker<Date>(calendarConfig);
|
|
5
|
+
type Interval={start:string;end:string};
|
|
6
|
+
export default function RangeFilter({id,month,value,onChange}:{id:string;month:boolean;value:Interval;onChange:(value:Interval)=>void}) {
|
|
7
|
+
const update=(_dates:unknown,strings:[string,string]|null)=>onChange({start:strings?.[0]||'',end:strings?.[1]||''});
|
|
8
|
+
return <CalendarPicker.RangePicker size="large" className="filter-date-range" id={{start:id+'-start',end:id+'-end'}} picker={month?'month':'date'} format={month?'YYYY-MM':'YYYY-MM-DD'} placeholder={month?['开始月份','结束月份']:['开始日期','结束日期']} allowEmpty={[true,true]} allowClear value={[calendarValue(value.start),calendarValue(value.end)]} minDate={calendarValue('0001-01-01')!} maxDate={calendarValue('9999-12-31')!} onCalendarChange={update} onChange={update} onKeyDown={e=>{if(e.key==='Enter')e.preventDefault();}}/>;
|
|
9
|
+
}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx
CHANGED
|
@@ -1,39 +1,40 @@
|
|
|
1
|
-
import {Component, lazy, Suspense, type ReactNode} from 'react';
|
|
2
|
-
import type {QueryResult, WidgetBinding} from '../finebi/types';
|
|
3
|
-
import {displayCell, selectDataset} from '../services/view-data';
|
|
4
|
-
import {DataTable} from './DataTable';
|
|
5
|
-
import type {WidgetState} from '../services/widget-queries';
|
|
6
|
-
import {formatMetric} from '../services/presentation';
|
|
7
|
-
const charts={bar:lazy(()=>import('./charts/Bar')),line:lazy(()=>import('./charts/Line')),pie:lazy(()=>import('./charts/Pie'))};
|
|
8
|
-
export class Boundary extends Component<{children: ReactNode;onRetry?:()=>void}, {error: string}> {
|
|
9
|
-
state = {error: ''};
|
|
10
|
-
static getDerivedStateFromError(error: Error) {return {error: error.message};}
|
|
11
|
-
render() {return this.state.error ? <div role="alert"><p>{this.state.error}</p>{this.props.onRetry&&<button onClick={this.props.onRetry}>重试组件</button>}</div> : this.props.children;}
|
|
12
|
-
}
|
|
13
|
-
function Content({result, binding, pageSize}: {result: QueryResult; binding: WidgetBinding; pageSize: number}) {
|
|
14
|
-
const data = selectDataset(result, binding.datasetId);
|
|
15
|
-
if (!data.rows.length) return <p className="empty">当前查询没有数据</p>;
|
|
16
|
-
if (binding.type === 'table') return <DataTable dataset={data} pageSize={pageSize} binding={binding}/>;
|
|
17
|
-
if (binding.type === 'kpi') {
|
|
18
|
-
const index = data.columns.findIndex(c => c.fieldId === binding.valueFieldId);
|
|
19
|
-
if (data.rows.length !== 1 || index < 0) throw new Error('KPI 需要单行指标结果,不能自动累加图表行');
|
|
20
|
-
return <div className="kpi" title={displayCell(data.rows[0].cells[index])}>{formatMetric(data.rows[0].cells[index],binding.valueFormat)}</div>;
|
|
21
|
-
}
|
|
22
|
-
const Chart=charts[binding.type];
|
|
23
|
-
return <Suspense fallback={<p role="status">正在加载图表…</p>}><Chart dataset={data} binding={binding}/></Suspense>;
|
|
24
|
-
}
|
|
25
|
-
export function Widget({state,binding,pageSize,onRetry,compact=false}: {state:WidgetState; binding:WidgetBinding;pageSize:number;onRetry:()=>void;compact?:boolean}) {
|
|
26
|
-
const result=state.result;
|
|
27
|
-
const selected=result?.datasets.find(d=>binding.datasetId?d.id===binding.datasetId:result.datasets.length===1);
|
|
28
|
-
return <section className={`panel widget-${binding.type} ${binding.type==='table'?'wide':''}`}><div className="panel-heading"><h2>{binding.name}</h2><span className={`component-status ${state.error?'failed':''}`}>{state.loading?'加载中':state.error?'查询失败':state.at?`更新于 ${state.at}`:'等待查询'}</span></div>
|
|
29
|
-
{binding.description&&<p className="muted">{binding.description}</p>}
|
|
30
|
-
{!compact&&binding.filterNames&&<p className="filter-scope">控件覆盖:{binding.filterNames.length?binding.filterNames.join('、'):'沿用源组件条件'}</p>}
|
|
31
|
-
{state.loading&&<p className="empty" role="status">正在获取实时数据…</p>}
|
|
32
|
-
{state.error&&<div className="component-error" role="alert"><p>{'code' in state.error?String(state.error.code)+' · ':''}{state.error.message}</p><button onClick={onRetry}>重试组件</button></div>}
|
|
33
|
-
{result&&<>
|
|
34
|
-
{compact?<details className="query-details"><summary>查询范围{selected?` · ${selected.returnedRowCount} 行`:''}</summary><p>{result.range.mode} · {result.normalization} · 后续数据:{String(result.range.hasNext)}</p><p>筛选作用:{binding.filterNames?.join('、')||'沿用源组件条件'}</p></details>:<p className="result-range">{result.range.mode} · 本次{selected?` ${selected.returnedRowCount} 行`:'多个模型,需显式选择'}{result.range.total.value!==null?` · 查询总行数 ${result.range.total.value}`:''} · hasNext={String(result.range.hasNext)} · {result.normalization}</p>}
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
1
|
+
import {Component, lazy, Suspense, type ReactNode} from 'react';
|
|
2
|
+
import type {QueryResult, WidgetBinding} from '../finebi/types';
|
|
3
|
+
import {displayCell, selectDataset} from '../services/view-data';
|
|
4
|
+
import {DataTable} from './DataTable';
|
|
5
|
+
import type {WidgetState} from '../services/widget-queries';
|
|
6
|
+
import {formatMetric} from '../services/presentation';
|
|
7
|
+
const charts={bar:lazy(()=>import('./charts/Bar')),line:lazy(()=>import('./charts/Line')),pie:lazy(()=>import('./charts/Pie'))};
|
|
8
|
+
export class Boundary extends Component<{children: ReactNode;onRetry?:()=>void}, {error: string}> {
|
|
9
|
+
state = {error: ''};
|
|
10
|
+
static getDerivedStateFromError(error: Error) {return {error: error.message};}
|
|
11
|
+
render() {return this.state.error ? <div role="alert"><p>{this.state.error}</p>{this.props.onRetry&&<button onClick={this.props.onRetry}>重试组件</button>}</div> : this.props.children;}
|
|
12
|
+
}
|
|
13
|
+
function Content({result, binding, pageSize}: {result: QueryResult; binding: WidgetBinding; pageSize: number}) {
|
|
14
|
+
const data = selectDataset(result, binding.datasetId);
|
|
15
|
+
if (!data.rows.length) return <p className="empty">当前查询没有数据</p>;
|
|
16
|
+
if (binding.type === 'table') return <DataTable dataset={data} pageSize={pageSize} binding={binding}/>;
|
|
17
|
+
if (binding.type === 'kpi') {
|
|
18
|
+
const index = data.columns.findIndex(c => c.fieldId === binding.valueFieldId);
|
|
19
|
+
if (data.rows.length !== 1 || index < 0) throw new Error('KPI 需要单行指标结果,不能自动累加图表行');
|
|
20
|
+
return <div className="kpi" title={displayCell(data.rows[0].cells[index])}>{formatMetric(data.rows[0].cells[index],binding.valueFormat)}</div>;
|
|
21
|
+
}
|
|
22
|
+
const Chart=charts[binding.type];
|
|
23
|
+
return <Suspense fallback={<p role="status">正在加载图表…</p>}><Chart dataset={data} binding={binding}/></Suspense>;
|
|
24
|
+
}
|
|
25
|
+
export function Widget({state,binding,pageSize,onRetry,compact=false}: {state:WidgetState; binding:WidgetBinding;pageSize:number;onRetry:()=>void;compact?:boolean}) {
|
|
26
|
+
const result=state.result;
|
|
27
|
+
const selected=result?.datasets.find(d=>binding.datasetId?d.id===binding.datasetId:result.datasets.length===1);
|
|
28
|
+
return <section className={`panel widget-${binding.type} ${binding.type==='table'?'wide':''}`}><div className="panel-heading"><h2>{binding.name}</h2><span className={`component-status ${state.error?'failed':''}`}>{state.loading?'加载中':state.error?'查询失败':state.at?`更新于 ${state.at}`:'等待查询'}</span></div>
|
|
29
|
+
{binding.description&&<p className="muted">{binding.description}</p>}
|
|
30
|
+
{!compact&&binding.filterNames&&<p className="filter-scope">控件覆盖:{binding.filterNames.length?binding.filterNames.join('、'):'沿用源组件条件'}</p>}
|
|
31
|
+
{state.loading&&<p className="empty" role="status">正在获取实时数据…</p>}
|
|
32
|
+
{state.error&&<div className="component-error" role="alert"><p>{'code' in state.error?String(state.error.code)+' · ':''}{state.error.message}</p><button onClick={onRetry}>重试组件</button></div>}
|
|
33
|
+
{result&&<>
|
|
34
|
+
{compact?<details className="query-details"><summary>查询范围{selected?` · ${selected.returnedRowCount} 行`:''}</summary><p>{result.range.mode} · {result.normalization} · 后续数据:{String(result.range.hasNext)}</p><p>筛选作用:{binding.filterNames?.join('、')||'沿用源组件条件'}</p></details>:<p className="result-range">{result.range.mode} · 本次{selected?` ${selected.returnedRowCount} 行`:'多个模型,需显式选择'}{result.range.total.value!==null?` · 查询总行数 ${result.range.total.value}`:''} · hasNext={String(result.range.hasNext)} · {result.normalization}</p>}
|
|
35
|
+
{result.range.axes&&<p className="result-range">交叉表当前视图 · 横向还有数据:{result.range.axes.horizontal.hasNext===null?'未知':result.range.axes.horizontal.hasNext?'是':'否'} · 纵向还有数据:{result.range.axes.vertical.hasNext===null?'未知':result.range.axes.vertical.hasNext?'是':'否'}</p>}
|
|
36
|
+
{(result.warnings.length>0||result.normalization!=='COMPLETE'||result.range.hasNext===true)&&<div className="notice" role="status">{result.range.hasNext||result.warnings.includes('NEXT_PAGE_UNSUPPORTED')?'当前仅展示首屏返回结果,本地分页不代表服务端续页。':'部分源图形未转换,当前展示已返回的数据模型。'}{compact?<details><summary>查看适配说明</summary><small>{result.normalization} · {result.warnings.join(' · ')}</small></details>:<small>{result.normalization} · {result.warnings.join(' · ')}</small>}</div>}
|
|
37
|
+
<Boundary key={state.at} onRetry={onRetry}><Content result={result} binding={binding} pageSize={pageSize}/></Boundary>
|
|
38
|
+
</>}
|
|
39
|
+
</section>;
|
|
40
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// Keep this list aligned with the languages configured by the application.
|
|
2
|
-
export {zhCN} from 'date-fns/locale/zh-CN';
|
|
3
|
-
export {enUS} from 'date-fns/locale/en-US';
|
|
1
|
+
// Keep this list aligned with the languages configured by the application.
|
|
2
|
+
export {zhCN} from 'date-fns/locale/zh-CN';
|
|
3
|
+
export {enUS} from 'date-fns/locale/en-US';
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import dateFnsConfig from '@rc-component/picker/generate/dateFns';
|
|
2
|
-
import {isDateValue} from '../finebi/date.js';
|
|
3
|
-
|
|
4
|
-
// Keep the picker on the same 0001–9999 calendar range as the filter contract.
|
|
5
|
-
export const calendarConfig = dateFnsConfig;
|
|
6
|
-
export function calendarValue(value:string):Date|null {
|
|
7
|
-
const fullDate = value.length===7 ? value+'-01' : value;
|
|
8
|
-
if (!isDateValue(fullDate)) return null;
|
|
9
|
-
const [year,month,day] = fullDate.split('-').map(Number);
|
|
10
|
-
const date = new Date(0);
|
|
11
|
-
date.setHours(0,0,0,0);
|
|
12
|
-
date.setFullYear(year,month-1,day);
|
|
13
|
-
return date;
|
|
14
|
-
}
|
|
1
|
+
import dateFnsConfig from '@rc-component/picker/generate/dateFns';
|
|
2
|
+
import {isDateValue} from '../finebi/date.js';
|
|
3
|
+
|
|
4
|
+
// Keep the picker on the same 0001–9999 calendar range as the filter contract.
|
|
5
|
+
export const calendarConfig = dateFnsConfig;
|
|
6
|
+
export function calendarValue(value:string):Date|null {
|
|
7
|
+
const fullDate = value.length===7 ? value+'-01' : value;
|
|
8
|
+
if (!isDateValue(fullDate)) return null;
|
|
9
|
+
const [year,month,day] = fullDate.split('-').map(Number);
|
|
10
|
+
const date = new Date(0);
|
|
11
|
+
date.setHours(0,0,0,0);
|
|
12
|
+
date.setFullYear(year,month-1,day);
|
|
13
|
+
return date;
|
|
14
|
+
}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Bar.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {use} from 'echarts/core';
|
|
2
|
-
import {BarChart} from 'echarts/charts';
|
|
3
|
-
import {GridComponent} from 'echarts/components';
|
|
4
|
-
|
|
5
|
-
use([BarChart, GridComponent]);
|
|
6
|
-
export {Chart as default} from '../Chart';
|
|
1
|
+
import {use} from 'echarts/core';
|
|
2
|
+
import {BarChart} from 'echarts/charts';
|
|
3
|
+
import {GridComponent} from 'echarts/components';
|
|
4
|
+
|
|
5
|
+
use([BarChart, GridComponent]);
|
|
6
|
+
export {Chart as default} from '../Chart';
|