@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
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Line.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {use} from 'echarts/core';
|
|
2
|
-
import {LineChart} from 'echarts/charts';
|
|
3
|
-
import {GridComponent} from 'echarts/components';
|
|
4
|
-
|
|
5
|
-
use([LineChart, GridComponent]);
|
|
6
|
-
export {Chart as default} from '../Chart';
|
|
1
|
+
import {use} from 'echarts/core';
|
|
2
|
+
import {LineChart} from 'echarts/charts';
|
|
3
|
+
import {GridComponent} from 'echarts/components';
|
|
4
|
+
|
|
5
|
+
use([LineChart, GridComponent]);
|
|
6
|
+
export {Chart as default} from '../Chart';
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/charts/Pie.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {use} from 'echarts/core';
|
|
2
|
-
import {PieChart} from 'echarts/charts';
|
|
3
|
-
import {LegendComponent} from 'echarts/components';
|
|
4
|
-
|
|
5
|
-
use([PieChart, LegendComponent]);
|
|
6
|
-
export {Chart as default} from '../Chart';
|
|
1
|
+
import {use} from 'echarts/core';
|
|
2
|
+
import {PieChart} from 'echarts/charts';
|
|
3
|
+
import {LegendComponent} from 'echarts/components';
|
|
4
|
+
|
|
5
|
+
use([PieChart, LegendComponent]);
|
|
6
|
+
export {Chart as default} from '../Chart';
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/month-picker.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.month-picker{position:relative;--month-accent:#197a58;--month-soft:#eaf6ef}.month-trigger{display:flex;justify-content:space-between;align-items:center;width:100%;height:46px;padding:0 14px;border:1px solid #dce4df;border-radius:10px;text-align:left;color:#87978f;background:#fff}.month-trigger.has-value{color:#204c3c}.month-trigger:focus-visible,.month-popover button:focus-visible{outline:3px solid #a1d6bc;outline-offset:2px}.month-trigger[aria-expanded=true]{border-color:var(--month-accent)}.month-dismiss{position:fixed;inset:0;z-index:20}.month-popover{position:absolute;right:0;top:calc(100% + 8px);z-index:21;width:min(310px,calc(100vw - 48px));padding:15px;border:1px solid #dce8e1;border-radius:15px;background:#fff;box-shadow:0 14px 45px #153f2926;color:#264c3d;max-height:65vh;overflow:auto}.month-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.month-popover button{border:0;background:transparent;border-radius:8px;padding:8px 12px}.month-head>button{font-size:19px}.month-head .month-year{font-size:14px;font-weight:700}.month-grid>[role=row]{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin:6px 0}.month-grid button{height:41px}.month-popover button:hover{background:var(--month-soft)}.month-grid button.selected{background:var(--month-accent);color:#fff}.month-actions{display:flex;justify-content:space-between;gap:4px;border-top:1px solid #e9efeb;margin-top:13px;padding-top:10px}.month-actions button{font-size:12px;color:var(--month-accent);padding:7px}.month-popover p{font-size:11px;color:#87978f;margin:9px 0 0;text-align:center}
|
|
1
|
+
.month-picker{position:relative;--month-accent:#197a58;--month-soft:#eaf6ef}.month-trigger{display:flex;justify-content:space-between;align-items:center;width:100%;height:46px;padding:0 14px;border:1px solid #dce4df;border-radius:10px;text-align:left;color:#87978f;background:#fff}.month-trigger.has-value{color:#204c3c}.month-trigger:focus-visible,.month-popover button:focus-visible{outline:3px solid #a1d6bc;outline-offset:2px}.month-trigger[aria-expanded=true]{border-color:var(--month-accent)}.month-dismiss{position:fixed;inset:0;z-index:20}.month-popover{position:absolute;right:0;top:calc(100% + 8px);z-index:21;width:min(310px,calc(100vw - 48px));padding:15px;border:1px solid #dce8e1;border-radius:15px;background:#fff;box-shadow:0 14px 45px #153f2926;color:#264c3d;max-height:65vh;overflow:auto}.month-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.month-popover button{border:0;background:transparent;border-radius:8px;padding:8px 12px}.month-head>button{font-size:19px}.month-head .month-year{font-size:14px;font-weight:700}.month-grid>[role=row]{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin:6px 0}.month-grid button{height:41px}.month-popover button:hover{background:var(--month-soft)}.month-grid button.selected{background:var(--month-accent);color:#fff}.month-actions{display:flex;justify-content:space-between;gap:4px;border-top:1px solid #e9efeb;margin-top:13px;padding-top:10px}.month-actions button{font-size:12px;color:var(--month-accent);padding:7px}.month-popover p{font-size:11px;color:#87978f;margin:9px 0 0;text-align:center}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {normalize} from './normalize';
|
|
2
2
|
import {encodeFilters} from './filters.js';
|
|
3
|
-
import {configErrors} from './config.js';
|
|
3
|
+
import {configErrors, sameDimensionIds} from './config.js';
|
|
4
4
|
import type {AppConfig, QueryResult, FilterState, WidgetBinding} from './types';
|
|
5
5
|
export class FineBIError extends Error {
|
|
6
6
|
constructor(public code: string, message: string) {super(message); this.name = 'FineBIError';}
|
|
@@ -14,10 +14,16 @@ export async function queryWidgets(config: AppConfig, filters: FilterState = {},
|
|
|
14
14
|
const refer = encodeFilters(config.filters,filters);
|
|
15
15
|
const ids = [...new Set(widgetIds ?? config.widgets.map(w => w.entryId))];
|
|
16
16
|
if(!ids.length||ids.some(id=>!config.widgets.some(w=>w.entryId===id)))throw new Error('组件不在配置范围内');
|
|
17
|
+
const dimensionSelections:Record<string,string[]> = Object.create(null);
|
|
18
|
+
for(const id of ids) {
|
|
19
|
+
const bindings=config.widgets.filter(w=>w.entryId===id);
|
|
20
|
+
if(bindings.some(w=>!sameDimensionIds(w.dimensionIds,bindings[0].dimensionIds)))throw new Error('同一组件有不同字段选择,请按展示绑定分别查询');
|
|
21
|
+
if(bindings[0].dimensionIds!==undefined)dimensionSelections[id]=bindings[0].dimensionIds;
|
|
22
|
+
}
|
|
17
23
|
const response = await fetchImpl(`${config.basePath.replace(/\/$/, '')}/v5/api/dashboard/report/${encodeURIComponent(config.dashboardId)}/widgets/query`, {
|
|
18
24
|
method: 'POST', credentials: 'include', signal, redirect: 'manual',
|
|
19
25
|
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
|
|
20
|
-
body: JSON.stringify({widgetIds: ids, includeWidgetBean: true, pageLimit: config.maxRows, refer,...(config.directoryEntryId?{reportEntryId:config.directoryEntryId}:{})}),
|
|
26
|
+
body: JSON.stringify({widgetIds: ids, includeWidgetBean: true, pageLimit: config.maxRows, refer,...(config.directoryEntryId?{reportEntryId:config.directoryEntryId}:{}),...(Object.keys(dimensionSelections).length?{dimensionSelections}:{})}),
|
|
21
27
|
});
|
|
22
28
|
if (response.status === 401 || response.type === 'opaqueredirect' || response.status >= 300 && response.status < 400) throw new FineBIError('AUTH_REQUIRED', '登录已失效,请登录 FineBI 后重试');
|
|
23
29
|
if (response.status === 403) throw new FineBIError('PERMISSION_DENIED', '当前用户没有访问权限');
|
|
@@ -26,7 +32,15 @@ export async function queryWidgets(config: AppConfig, filters: FilterState = {},
|
|
|
26
32
|
const code = body.errorCode ?? body.code;
|
|
27
33
|
if (['TIME_OUT','JWT_EXPIRED','LOGIN_INFO_NOT_AVAILABLE','CANNOT_GET_TOKEN','AUTH_REQUIRED'].includes(code)) throw new FineBIError('AUTH_REQUIRED', '登录已失效,请登录 FineBI 后重试');
|
|
28
34
|
if (code === 'PERMISSION_DENIED') throw new FineBIError('PERMISSION_DENIED', '当前用户没有数据或字段权限');
|
|
29
|
-
|
|
35
|
+
const developmentAuthErrors: Record<string,string> = {
|
|
36
|
+
AUTH_PLUGIN_REQUIRED:'请更新 FineBI 服务端插件以启用浏览器授权',
|
|
37
|
+
AUTH_HTTPS_REQUIRED:'浏览器授权需要 HTTPS FineBI 地址',
|
|
38
|
+
AUTH_PROTOCOL_ERROR:'认证响应不符合预期,请检查 FineBI 地址和版本',
|
|
39
|
+
AUTH_UNAVAILABLE:'授权服务暂不可用,请稍后重试',
|
|
40
|
+
AUTH_STORAGE_FAILED:'无法保存本地会话,请检查 .env.local 文件权限',
|
|
41
|
+
};
|
|
42
|
+
if (developmentAuthErrors[code]) throw new FineBIError(code, `${developmentAuthErrors[code]};详情见开发终端`);
|
|
43
|
+
if (!response.ok || body.success !== true) throw new FineBIError(typeof code==='string'?code:'QUERY_FAILED', 'FineBI 查询失败,请检查组件、控件绑定、平台版本及访问权限');
|
|
30
44
|
const results: Record<string, QueryResult> = {};
|
|
31
45
|
for (const id of ids) {
|
|
32
46
|
if (!body.data?.[id]?.previewResult) throw new FineBIError('INVALID_RESPONSE', `接口未返回组件 ${id},不能视为空数据`);
|
|
@@ -35,8 +49,10 @@ export async function queryWidgets(config: AppConfig, filters: FilterState = {},
|
|
|
35
49
|
return results;
|
|
36
50
|
}
|
|
37
51
|
export async function queryWidget(config:AppConfig,binding:WidgetBinding,filters:FilterState={},signal?:AbortSignal,fetchImpl:typeof fetch=fetch):Promise<QueryResult> {
|
|
52
|
+
validateConfig(config);
|
|
53
|
+
if(!config.widgets.includes(binding))throw new Error('展示绑定不在配置范围内');
|
|
38
54
|
// Validate all draft names before projecting the verified component scope.
|
|
39
55
|
encodeFilters(config.filters,filters);
|
|
40
56
|
const scoped=binding.filterNames ? Object.fromEntries(Object.entries(filters).filter(([name])=>binding.filterNames!.includes(name))) : filters;
|
|
41
|
-
return (await queryWidgets(config,scoped,signal,fetchImpl,[binding.entryId]))[binding.entryId];
|
|
57
|
+
return (await queryWidgets({...config,widgets:[binding]},scoped,signal,fetchImpl,[binding.entryId]))[binding.entryId];
|
|
42
58
|
}
|
|
@@ -1 +1,3 @@
|
|
|
1
|
-
export function configErrors(config: unknown): string[];
|
|
1
|
+
export function configErrors(config: unknown): string[];
|
|
2
|
+
export function sameDimensionIds(left: string[] | undefined, right: string[] | undefined): boolean;
|
|
3
|
+
export function validateDimensionSelections(widgets: {entryId: string; dimensionIds?: string[]}[], widgetIds: string[], selections: unknown): void;
|
|
@@ -1,44 +1,58 @@
|
|
|
1
|
-
import {validateFilterBindings} from './filters.js';
|
|
2
|
-
const nonempty=value=>typeof value==='string'&&value.trim().length>0;
|
|
3
|
-
const record=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if(!
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
1
|
+
import {validateFilterBindings} from './filters.js';
|
|
2
|
+
const nonempty=value=>typeof value==='string'&&value.trim().length>0;
|
|
3
|
+
const record=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
4
|
+
const validDimensionIds=value=>Array.isArray(value)&&value.every(nonempty)&&new Set(value).size===value.length;
|
|
5
|
+
export function sameDimensionIds(left,right) {
|
|
6
|
+
if(left===undefined||right===undefined)return left===right;
|
|
7
|
+
return validDimensionIds(left)&&validDimensionIds(right)&&left.length===right.length&&left.every(id=>right.includes(id));
|
|
8
|
+
}
|
|
9
|
+
export function validateDimensionSelections(widgets,widgetIds,selections) {
|
|
10
|
+
if(selections!==undefined&&(!record(selections)||Object.keys(selections).some(id=>!widgetIds.includes(id))))throw new Error('Invalid dimension selections');
|
|
11
|
+
for(const id of widgetIds) {
|
|
12
|
+
const selected=selections&&Object.hasOwn(selections,id)?selections[id]:undefined;
|
|
13
|
+
if(selected!==undefined&&!validDimensionIds(selected))throw new Error('Invalid dimensionIds');
|
|
14
|
+
if(!widgets.some(w=>w.entryId===id&&sameDimensionIds(w.dimensionIds,selected)))throw new Error('Unconfigured dimension selection');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function configErrors(config) {
|
|
18
|
+
if(!record(config))return ['configuration must be a JSON object.'];
|
|
19
|
+
const errors=[];
|
|
20
|
+
const allowed=['schemaVersion','title','basePath','layout','dashboardId','directoryEntryId','maxRows','pageSize','widgets','filters','templateVersion','description','insights','dataNotes'];
|
|
21
|
+
for(const key of Object.keys(config))if(!allowed.includes(key))errors.push(`unsupported config field: ${key}.`);
|
|
22
|
+
if(config.schemaVersion!=='1')errors.push('schemaVersion must be "1".');
|
|
23
|
+
if(!nonempty(config.title))errors.push('title is required.');
|
|
24
|
+
if(!nonempty(config.basePath)||!/^\/(?!\/)[\w/-]+$/.test(config.basePath))errors.push('basePath must be a same-origin path.');
|
|
25
|
+
if(!['overview','workbench','cockpit','retail'].includes(config.layout))errors.push('layout must be overview, workbench, cockpit or retail.');
|
|
26
|
+
if(!nonempty(config.dashboardId))errors.push('project is not bound: dashboardId must be configured.');
|
|
27
|
+
if('directoryEntryId' in config&&!nonempty(config.directoryEntryId))errors.push('directoryEntryId must be a non-empty string.');
|
|
28
|
+
if(!Number.isSafeInteger(config.maxRows)||config.maxRows<1||config.maxRows>10000)errors.push('maxRows must be an integer from 1 to 10000.');
|
|
29
|
+
if(!Number.isSafeInteger(config.pageSize)||config.pageSize<1||config.pageSize>100)errors.push('pageSize must be an integer from 1 to 100.');
|
|
30
|
+
if('description' in config&&!nonempty(config.description))errors.push('description must be a non-empty string.');
|
|
31
|
+
if('insights' in config&&typeof config.insights!=='boolean')errors.push('insights must be boolean.');
|
|
32
|
+
if('dataNotes' in config&&(!Array.isArray(config.dataNotes)||config.dataNotes.some(v=>!nonempty(v))))errors.push('dataNotes must be text array.');
|
|
33
|
+
try{validateFilterBindings(config.filters);}catch(error){errors.push(error.message);}
|
|
34
|
+
if(!Array.isArray(config.widgets)||!config.widgets.length)return [...errors,'widgets must be a non-empty array.'];
|
|
35
|
+
for(const [i,w] of config.widgets.entries()) {
|
|
36
|
+
if(!record(w)){errors.push(`widgets[${i}] must be an object.`);continue;}
|
|
37
|
+
const fields=['entryId','name','type','datasetId','categoryFieldId','valueFieldId','series','description','filterNames','tableFieldIds','localFilterFieldIds','valueFormat','categoryFormat','categoryTimeZone','dimensionIds'];
|
|
38
|
+
for(const field of Object.keys(w))if(!fields.includes(field))errors.push(`widgets[${i}].${field} is unsupported.`);
|
|
39
|
+
for(const field of ['entryId','name'])if(!nonempty(w[field]))errors.push(`widgets[${i}].${field} is required.`);
|
|
40
|
+
if('dimensionIds' in w&&!validDimensionIds(w.dimensionIds))errors.push(`widgets[${i}].dimensionIds must be unique non-empty strings.`);
|
|
41
|
+
if(!['table','kpi','bar','line','pie'].includes(w.type))errors.push(`widgets[${i}].type is unsupported.`);
|
|
42
|
+
if('valueFormat' in w&&(w.type!=='kpi'||!['number','percent'].includes(w.valueFormat)))errors.push(`widgets[${i}].valueFormat requires a KPI and number or percent.`);
|
|
43
|
+
if('categoryFormat' in w||'categoryTimeZone' in w) {
|
|
44
|
+
if(!['line','bar'].includes(w.type)||w.categoryFormat!=='epoch-month'||!nonempty(w.categoryTimeZone))errors.push(`widgets[${i}].categoryFormat requires a line/bar, epoch-month and an explicit categoryTimeZone.`);
|
|
45
|
+
else try{new Intl.DateTimeFormat('en',{timeZone:w.categoryTimeZone});}catch{errors.push(`widgets[${i}].categoryTimeZone is invalid.`);}
|
|
46
|
+
}
|
|
47
|
+
for(const field of ['datasetId','categoryFieldId','valueFieldId','description'])if(field in w&&!nonempty(w[field]))errors.push(`widgets[${i}].${field} must be non-empty.`);
|
|
48
|
+
if(w.type!=='table'&&!nonempty(w.valueFieldId)&&!w.series)errors.push(`widgets[${i}].valueFieldId is required.`);
|
|
49
|
+
if(!['table','kpi'].includes(w.type)&&!nonempty(w.categoryFieldId))errors.push(`widgets[${i}].categoryFieldId is required.`);
|
|
50
|
+
if('series' in w&&(!['line','bar'].includes(w.type)||!Array.isArray(w.series)||!w.series.length||w.series.some(s=>!record(s)||Object.keys(s).some(k=>!['fieldId','label'].includes(k))||!nonempty(s.fieldId)||!nonempty(s.label))||new Set(w.series.map(s=>s?.fieldId)).size!==w.series.length))errors.push(`widgets[${i}].series is invalid.`);
|
|
51
|
+
for(const field of ['filterNames','tableFieldIds','localFilterFieldIds'])if(field in w) {
|
|
52
|
+
if(!Array.isArray(w[field])||w[field].some(v=>!nonempty(v))||new Set(w[field]).size!==w[field].length)errors.push(`widgets[${i}].${field} must be unique strings.`);
|
|
53
|
+
else if(field==='filterNames'&&w[field].some(name=>!Array.isArray(config.filters)||!config.filters.some(f=>f?.name===name)))errors.push(`widgets[${i}].filterNames references an unknown filter.`);
|
|
54
|
+
else if(field!=='filterNames'&&(w.type!=='table'||!w[field].length))errors.push(`widgets[${i}].${field} is only for tables and must be non-empty.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return errors;
|
|
58
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export function isDateValue(value: unknown): boolean;
|
|
1
|
+
export function isDateValue(value: unknown): boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export function isDateValue(value) {
|
|
2
|
-
if (typeof value !== 'string' || !/^(?!0000)\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
3
|
-
const date = new Date(`${value}T00:00:00.000Z`);
|
|
4
|
-
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
5
|
-
}
|
|
1
|
+
export function isDateValue(value) {
|
|
2
|
+
if (typeof value !== 'string' || !/^(?!0000)\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
3
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
4
|
+
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
|
5
|
+
}
|
package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {FilterBinding, FilterState} from './types';
|
|
2
|
-
export function validateFilterBindings(bindings: unknown): void;
|
|
3
|
-
export function encodeFilters(bindings: FilterBinding[], state?: FilterState): Record<string,string[]>;
|
|
4
|
-
export function validateRefer(bindings: FilterBinding[], refer?: unknown): void;
|
|
1
|
+
import type {FilterBinding, FilterState} from './types';
|
|
2
|
+
export function validateFilterBindings(bindings: unknown): void;
|
|
3
|
+
export function encodeFilters(bindings: FilterBinding[], state?: FilterState): Record<string,string[]>;
|
|
4
|
+
export function validateRefer(bindings: FilterBinding[], refer?: unknown): void;
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
import {isDateValue} from './date.js';
|
|
2
|
-
|
|
3
|
-
const types = new Set(['text','select','multi-select','number','date','date-range','year','month','month-range','quarter','time-label']);
|
|
4
|
-
const choices = new Set(['select','multi-select','time-label']);
|
|
5
|
-
const nullable = new Set(['text','select','multi-select','number']);
|
|
6
|
-
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
-
const nonempty = value => typeof value === 'string' && value.trim().length > 0;
|
|
8
|
-
const literal = value => typeof value === 'string' && value !== '' && value !== 'null' && !value.includes(',');
|
|
9
|
-
const kind = binding => binding.type ?? 'select';
|
|
10
|
-
const range = binding => ['date-range','month-range'].includes(kind(binding));
|
|
11
|
-
const keys = binding => range(binding) ? [binding.name+'_s',binding.name+'_e'] : [binding.name];
|
|
12
|
-
const fail = message => {throw new Error(`无效筛选:${message}`);};
|
|
13
|
-
|
|
14
|
-
// Shared by app inspect, browser requests and the development proxy.
|
|
15
|
-
export function validateFilterBindings(bindings) {
|
|
16
|
-
if (!Array.isArray(bindings)) fail('filters 必须为数组');
|
|
17
|
-
const names = new Set(), wireKeys = new Set();
|
|
18
|
-
for (const binding of bindings) {
|
|
19
|
-
if (!record(binding)) fail('配置必须为对象');
|
|
20
|
-
for (const key of Object.keys(binding)) if (!['name','label','type','options','allowNull'].includes(key)) fail(`不支持的配置字段 ${key}`);
|
|
21
|
-
if (!nonempty(binding.name) || !nonempty(binding.label)) fail('name 和 label 必须为非空文本');
|
|
22
|
-
if (names.has(binding.name)) fail(`重复名称 ${binding.name}`);
|
|
23
|
-
names.add(binding.name);
|
|
24
|
-
if (!types.has(kind(binding))) fail(`不支持的类型 ${binding.type}`);
|
|
25
|
-
if ('allowNull' in binding && (typeof binding.allowNull !== 'boolean' || !nullable.has(kind(binding)))) fail(`${binding.name} 不支持此 allowNull 配置`);
|
|
26
|
-
for (const key of keys(binding)) {
|
|
27
|
-
if (wireKeys.has(key)) fail(`请求键冲突 ${key}`);
|
|
28
|
-
wireKeys.add(key);
|
|
29
|
-
}
|
|
30
|
-
if (!choices.has(kind(binding))) {
|
|
31
|
-
if ('options' in binding) fail(`${binding.name} 不配置 options`);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (!Array.isArray(binding.options)) fail(`${binding.name} 必须配置 options`);
|
|
35
|
-
const values = new Set();
|
|
36
|
-
for (const option of binding.options) {
|
|
37
|
-
if (!record(option) || Object.keys(option).some(key => !['label','value'].includes(key)) || !nonempty(option.label)
|
|
38
|
-
|| !(kind(binding)==='time-label' ? nonempty(option.value) : literal(option.value))) fail(`${binding.name} 含无法编码的选项(空串、逗号或保留字 null)`);
|
|
39
|
-
if (values.has(option.value)) fail(`${binding.name} 含重复选项`);
|
|
40
|
-
values.add(option.value);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function temporal(type, value) {
|
|
46
|
-
if (typeof value !== 'string') return false;
|
|
47
|
-
if (type === 'date' || type === 'date-range') return isDateValue(value);
|
|
48
|
-
if (type === 'year') return /^\d{4}$/.test(value) && value !== '0000';
|
|
49
|
-
if (type === 'quarter') return /^\d{4}-[1-4]$/.test(value) && !value.startsWith('0000-');
|
|
50
|
-
return /^\d{4}-(0[1-9]|1[0-2])$/.test(value) && !value.startsWith('0000-');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function encodeFilters(bindings, state = {}) {
|
|
54
|
-
validateFilterBindings(bindings);
|
|
55
|
-
if (!record(state)) fail('状态必须为对象');
|
|
56
|
-
const result = Object.create(null);
|
|
57
|
-
for (const [name, value] of Object.entries(state)) {
|
|
58
|
-
const binding = bindings.find(item => item.name === name);
|
|
59
|
-
if (!binding) fail(`未知名称 ${name}`);
|
|
60
|
-
const type = kind(binding);
|
|
61
|
-
// Empty UI values mean saved conditions, never an unrestricted query.
|
|
62
|
-
if (value === undefined || value === '') continue;
|
|
63
|
-
if (value === null) {
|
|
64
|
-
if (!binding.allowNull) fail(`${name} 不支持筛选空值`);
|
|
65
|
-
result[name] = ['null'];
|
|
66
|
-
} else if (range(binding)) {
|
|
67
|
-
if (!record(value) || Object.keys(value).some(key => !['start','end'].includes(key))
|
|
68
|
-
|| typeof value.start !== 'string' || typeof value.end !== 'string') fail(`${name} 需要 start/end`);
|
|
69
|
-
const {start,end} = value;
|
|
70
|
-
if (start && !temporal(type,start) || end && !temporal(type,end) || start && end && start > end) fail(`${name} 区间无效或起始晚于结束`);
|
|
71
|
-
if (start) result[name+'_s'] = [start];
|
|
72
|
-
if (end) result[name+'_e'] = [end];
|
|
73
|
-
} else if (type === 'multi-select') {
|
|
74
|
-
if (!Array.isArray(value) || value.some(v => !literal(v) || !binding.options.some(o => o.value === v)) || new Set(value).size !== value.length) fail(`${name} 多选值无效`);
|
|
75
|
-
if (value.length) result[name] = [value.join(',')];
|
|
76
|
-
} else {
|
|
77
|
-
const valid = type === 'text' ? literal(value)
|
|
78
|
-
: type === 'number' ? typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value)
|
|
79
|
-
: choices.has(type) ? typeof value === 'string' && binding.options.some(o => o.value === value)
|
|
80
|
-
: temporal(type,value);
|
|
81
|
-
if (!valid) fail(`${name} 值格式不正确`);
|
|
82
|
-
result[name] = [value];
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return {...result};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function validateRefer(bindings, refer = {}) {
|
|
89
|
-
validateFilterBindings(bindings);
|
|
90
|
-
if (!record(refer)) fail('refer 必须为对象');
|
|
91
|
-
const known = new Set(bindings.flatMap(keys));
|
|
92
|
-
for (const [name,value] of Object.entries(refer)) {
|
|
93
|
-
if (!known.has(name) || !Array.isArray(value) || value.length !== 1 || typeof value[0] !== 'string' || !value[0]) fail(`非法请求键或值 ${name}`);
|
|
94
|
-
}
|
|
95
|
-
const state = Object.create(null);
|
|
96
|
-
for (const binding of bindings) {
|
|
97
|
-
if (range(binding)) {
|
|
98
|
-
const start = refer[binding.name+'_s']?.[0] ?? '', end = refer[binding.name+'_e']?.[0] ?? '';
|
|
99
|
-
if (start || end) state[binding.name] = {start,end};
|
|
100
|
-
} else if (Object.hasOwn(refer,binding.name)) {
|
|
101
|
-
const value = refer[binding.name][0];
|
|
102
|
-
state[binding.name] = value === 'null' && nullable.has(kind(binding)) ? null : kind(binding) === 'multi-select' ? value.split(',') : value;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
encodeFilters(bindings,state);
|
|
106
|
-
}
|
|
1
|
+
import {isDateValue} from './date.js';
|
|
2
|
+
|
|
3
|
+
const types = new Set(['text','select','multi-select','number','date','date-range','year','month','month-range','quarter','time-label']);
|
|
4
|
+
const choices = new Set(['select','multi-select','time-label']);
|
|
5
|
+
const nullable = new Set(['text','select','multi-select','number']);
|
|
6
|
+
const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
const nonempty = value => typeof value === 'string' && value.trim().length > 0;
|
|
8
|
+
const literal = value => typeof value === 'string' && value !== '' && value !== 'null' && !value.includes(',');
|
|
9
|
+
const kind = binding => binding.type ?? 'select';
|
|
10
|
+
const range = binding => ['date-range','month-range'].includes(kind(binding));
|
|
11
|
+
const keys = binding => range(binding) ? [binding.name+'_s',binding.name+'_e'] : [binding.name];
|
|
12
|
+
const fail = message => {throw new Error(`无效筛选:${message}`);};
|
|
13
|
+
|
|
14
|
+
// Shared by app inspect, browser requests and the development proxy.
|
|
15
|
+
export function validateFilterBindings(bindings) {
|
|
16
|
+
if (!Array.isArray(bindings)) fail('filters 必须为数组');
|
|
17
|
+
const names = new Set(), wireKeys = new Set();
|
|
18
|
+
for (const binding of bindings) {
|
|
19
|
+
if (!record(binding)) fail('配置必须为对象');
|
|
20
|
+
for (const key of Object.keys(binding)) if (!['name','label','type','options','allowNull'].includes(key)) fail(`不支持的配置字段 ${key}`);
|
|
21
|
+
if (!nonempty(binding.name) || !nonempty(binding.label)) fail('name 和 label 必须为非空文本');
|
|
22
|
+
if (names.has(binding.name)) fail(`重复名称 ${binding.name}`);
|
|
23
|
+
names.add(binding.name);
|
|
24
|
+
if (!types.has(kind(binding))) fail(`不支持的类型 ${binding.type}`);
|
|
25
|
+
if ('allowNull' in binding && (typeof binding.allowNull !== 'boolean' || !nullable.has(kind(binding)))) fail(`${binding.name} 不支持此 allowNull 配置`);
|
|
26
|
+
for (const key of keys(binding)) {
|
|
27
|
+
if (wireKeys.has(key)) fail(`请求键冲突 ${key}`);
|
|
28
|
+
wireKeys.add(key);
|
|
29
|
+
}
|
|
30
|
+
if (!choices.has(kind(binding))) {
|
|
31
|
+
if ('options' in binding) fail(`${binding.name} 不配置 options`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(binding.options)) fail(`${binding.name} 必须配置 options`);
|
|
35
|
+
const values = new Set();
|
|
36
|
+
for (const option of binding.options) {
|
|
37
|
+
if (!record(option) || Object.keys(option).some(key => !['label','value'].includes(key)) || !nonempty(option.label)
|
|
38
|
+
|| !(kind(binding)==='time-label' ? nonempty(option.value) : literal(option.value))) fail(`${binding.name} 含无法编码的选项(空串、逗号或保留字 null)`);
|
|
39
|
+
if (values.has(option.value)) fail(`${binding.name} 含重复选项`);
|
|
40
|
+
values.add(option.value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function temporal(type, value) {
|
|
46
|
+
if (typeof value !== 'string') return false;
|
|
47
|
+
if (type === 'date' || type === 'date-range') return isDateValue(value);
|
|
48
|
+
if (type === 'year') return /^\d{4}$/.test(value) && value !== '0000';
|
|
49
|
+
if (type === 'quarter') return /^\d{4}-[1-4]$/.test(value) && !value.startsWith('0000-');
|
|
50
|
+
return /^\d{4}-(0[1-9]|1[0-2])$/.test(value) && !value.startsWith('0000-');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function encodeFilters(bindings, state = {}) {
|
|
54
|
+
validateFilterBindings(bindings);
|
|
55
|
+
if (!record(state)) fail('状态必须为对象');
|
|
56
|
+
const result = Object.create(null);
|
|
57
|
+
for (const [name, value] of Object.entries(state)) {
|
|
58
|
+
const binding = bindings.find(item => item.name === name);
|
|
59
|
+
if (!binding) fail(`未知名称 ${name}`);
|
|
60
|
+
const type = kind(binding);
|
|
61
|
+
// Empty UI values mean saved conditions, never an unrestricted query.
|
|
62
|
+
if (value === undefined || value === '') continue;
|
|
63
|
+
if (value === null) {
|
|
64
|
+
if (!binding.allowNull) fail(`${name} 不支持筛选空值`);
|
|
65
|
+
result[name] = ['null'];
|
|
66
|
+
} else if (range(binding)) {
|
|
67
|
+
if (!record(value) || Object.keys(value).some(key => !['start','end'].includes(key))
|
|
68
|
+
|| typeof value.start !== 'string' || typeof value.end !== 'string') fail(`${name} 需要 start/end`);
|
|
69
|
+
const {start,end} = value;
|
|
70
|
+
if (start && !temporal(type,start) || end && !temporal(type,end) || start && end && start > end) fail(`${name} 区间无效或起始晚于结束`);
|
|
71
|
+
if (start) result[name+'_s'] = [start];
|
|
72
|
+
if (end) result[name+'_e'] = [end];
|
|
73
|
+
} else if (type === 'multi-select') {
|
|
74
|
+
if (!Array.isArray(value) || value.some(v => !literal(v) || !binding.options.some(o => o.value === v)) || new Set(value).size !== value.length) fail(`${name} 多选值无效`);
|
|
75
|
+
if (value.length) result[name] = [value.join(',')];
|
|
76
|
+
} else {
|
|
77
|
+
const valid = type === 'text' ? literal(value)
|
|
78
|
+
: type === 'number' ? typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value)
|
|
79
|
+
: choices.has(type) ? typeof value === 'string' && binding.options.some(o => o.value === value)
|
|
80
|
+
: temporal(type,value);
|
|
81
|
+
if (!valid) fail(`${name} 值格式不正确`);
|
|
82
|
+
result[name] = [value];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {...result};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function validateRefer(bindings, refer = {}) {
|
|
89
|
+
validateFilterBindings(bindings);
|
|
90
|
+
if (!record(refer)) fail('refer 必须为对象');
|
|
91
|
+
const known = new Set(bindings.flatMap(keys));
|
|
92
|
+
for (const [name,value] of Object.entries(refer)) {
|
|
93
|
+
if (!known.has(name) || !Array.isArray(value) || value.length !== 1 || typeof value[0] !== 'string' || !value[0]) fail(`非法请求键或值 ${name}`);
|
|
94
|
+
}
|
|
95
|
+
const state = Object.create(null);
|
|
96
|
+
for (const binding of bindings) {
|
|
97
|
+
if (range(binding)) {
|
|
98
|
+
const start = refer[binding.name+'_s']?.[0] ?? '', end = refer[binding.name+'_e']?.[0] ?? '';
|
|
99
|
+
if (start || end) state[binding.name] = {start,end};
|
|
100
|
+
} else if (Object.hasOwn(refer,binding.name)) {
|
|
101
|
+
const value = refer[binding.name][0];
|
|
102
|
+
state[binding.name] = value === 'null' && nullable.has(kind(binding)) ? null : kind(binding) === 'multi-select' ? value.split(',') : value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
encodeFilters(bindings,state);
|
|
106
|
+
}
|