@liujitcn/kratos-admin-bi 0.0.0-stage → 0.1.6
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 +43 -2
- package/dist/package/declarations/index.d.ts +1 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_card.d.ts +18 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_card.js +46 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_dashboard.d.ts +17 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_dashboard.js +30 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_query.d.ts +11 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_query.js +41 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_query_record.d.ts +10 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_query_record.js +14 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_source.d.ts +26 -0
- package/dist/package/declarations/src/api/bi/admin/v1/bi_source.js +46 -0
- package/dist/package/declarations/src/bi-core.d.ts +17 -0
- package/dist/package/declarations/src/bi-core.js +22 -0
- package/dist/package/declarations/src/bi-dashboard-schema.d.ts +31 -0
- package/dist/package/declarations/src/bi-dashboard-schema.js +39 -0
- package/dist/package/declarations/src/bi-messages.d.ts +247 -0
- package/dist/package/declarations/src/bi-messages.js +247 -0
- package/dist/package/declarations/src/bi-response.d.ts +10 -0
- package/dist/package/declarations/src/bi-response.js +33 -0
- package/dist/package/declarations/src/bi-vue.d.ts +10 -0
- package/dist/package/declarations/src/bi-vue.js +10 -0
- package/dist/package/declarations/src/card-schema.d.ts +30 -0
- package/dist/package/declarations/src/card-schema.js +87 -0
- package/dist/package/declarations/src/components/BiCardChartEditor.vue.d.ts +17 -0
- package/dist/package/declarations/src/components/BiCardChartEditor.vue.js +527 -0
- package/dist/package/declarations/src/components/BiCardFormDialog.vue.d.ts +20 -0
- package/dist/package/declarations/src/components/BiCardFormDialog.vue.js +495 -0
- package/dist/package/declarations/src/components/BiCardLibrary.vue.d.ts +3 -0
- package/dist/package/declarations/src/components/BiCardLibrary.vue.js +807 -0
- package/dist/package/declarations/src/components/BiCardVisualization.vue.d.ts +10 -0
- package/dist/package/declarations/src/components/BiCardVisualization.vue.js +97 -0
- package/dist/package/declarations/src/components/BiChart.vue.d.ts +20 -0
- package/dist/package/declarations/src/components/BiChart.vue.js +112 -0
- package/dist/package/declarations/src/components/BiDashboard.vue.d.ts +9 -0
- package/dist/package/declarations/src/components/BiDashboard.vue.js +81 -0
- package/dist/package/declarations/src/components/BiDashboardMenuDialog.vue.d.ts +12 -0
- package/dist/package/declarations/src/components/BiDashboardMenuDialog.vue.js +252 -0
- package/dist/package/declarations/src/components/BiDataCard.vue.d.ts +36 -0
- package/dist/package/declarations/src/components/BiDataCard.vue.js +372 -0
- package/dist/package/declarations/src/components/BiDesigner.vue.d.ts +17 -0
- package/dist/package/declarations/src/components/BiDesigner.vue.js +666 -0
- package/dist/package/declarations/src/components/BiSourceModelDialog.vue.d.ts +17 -0
- package/dist/package/declarations/src/components/BiSourceModelDialog.vue.js +2118 -0
- package/dist/package/declarations/src/components/BiWorkbenchPreview.vue.d.ts +7 -0
- package/dist/package/declarations/src/components/BiWorkbenchPreview.vue.js +176 -0
- package/dist/package/declarations/src/components/source-model-layout.d.ts +3 -0
- package/dist/package/declarations/src/components/source-model-layout.js +66 -0
- package/dist/package/declarations/src/dataset/index.d.ts +7 -0
- package/dist/package/declarations/src/dataset/index.js +25 -0
- package/dist/package/declarations/src/dataset/labels.d.ts +2 -0
- package/dist/package/declarations/src/dataset/labels.js +21 -0
- package/dist/package/declarations/src/dataset/transform.d.ts +20 -0
- package/dist/package/declarations/src/dataset/transform.js +173 -0
- package/dist/package/declarations/src/engine/chart-types.d.ts +14 -0
- package/dist/package/declarations/src/engine/chart-types.js +37 -0
- package/dist/package/declarations/src/engine/echarts.d.ts +16 -0
- package/dist/package/declarations/src/engine/echarts.js +491 -0
- package/dist/package/declarations/src/engine/types.d.ts +36 -0
- package/dist/package/declarations/src/engine/types.js +20 -0
- package/dist/package/declarations/src/env.d.ts +17 -0
- package/dist/package/declarations/src/env.js +67 -0
- package/dist/package/declarations/src/index.d.ts +1 -0
- package/dist/package/declarations/src/index.js +1 -0
- package/dist/package/declarations/src/locales/en-US.json +254 -0
- package/dist/package/declarations/src/locales/generated.d.ts +1021 -0
- package/dist/package/declarations/src/locales/generated.js +13 -0
- package/dist/package/declarations/src/locales/ja-JP.json +254 -0
- package/dist/package/declarations/src/locales/zh-CN.json +254 -0
- package/dist/package/declarations/src/locales/zh-TW.json +254 -0
- package/dist/package/declarations/src/module.d.ts +2 -0
- package/dist/package/declarations/src/module.js +9 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_card.d.ts +145 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_card.js +6 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_dashboard.d.ts +107 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_dashboard.js +6 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_query.d.ts +157 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_query.js +6 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_query_record.d.ts +83 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_query_record.js +15 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_source.d.ts +301 -0
- package/dist/package/declarations/src/rpc/bi/admin/v1/bi_source.js +6 -0
- package/dist/package/declarations/src/rpc/common/v1/types.d.ts +60 -0
- package/dist/package/declarations/src/rpc/common/v1/types.js +6 -0
- package/dist/package/declarations/src/schema/layout.d.ts +7 -0
- package/dist/package/declarations/src/schema/layout.js +52 -0
- package/dist/package/declarations/src/schema/types.d.ts +83 -0
- package/dist/package/declarations/src/schema/types.js +1 -0
- package/dist/package/declarations/src/theme/index.d.ts +29 -0
- package/dist/package/declarations/src/theme/index.js +83 -0
- package/dist/package/declarations/src/theme/palettes.d.ts +7 -0
- package/dist/package/declarations/src/theme/palettes.js +28 -0
- package/dist/package/declarations/src/theme/presets.d.ts +73 -0
- package/dist/package/declarations/src/theme/presets.js +235 -0
- package/dist/package/declarations/src/views/card/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/card/index.vue.js +18 -0
- package/dist/package/declarations/src/views/dashboard/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/dashboard/index.vue.js +107 -0
- package/dist/package/declarations/src/views/designer/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/designer/index.vue.js +332 -0
- package/dist/package/declarations/src/views/manage/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/manage/index.vue.js +325 -0
- package/dist/package/declarations/src/views/query/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/query/index.vue.js +876 -0
- package/dist/package/declarations/src/views/source/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/source/index.vue.js +343 -0
- package/dist/package/declarations/src/views/workbench/index.vue.d.ts +3 -0
- package/dist/package/declarations/src/views/workbench/index.vue.js +362 -0
- package/dist/package/src/api/bi/admin/v1/bi_card.ts +67 -0
- package/dist/package/src/api/bi/admin/v1/bi_dashboard.ts +51 -0
- package/dist/package/src/api/bi/admin/v1/bi_query.ts +55 -0
- package/dist/package/src/api/bi/admin/v1/bi_query_record.ts +25 -0
- package/dist/package/src/api/bi/admin/v1/bi_source.ts +80 -0
- package/dist/package/src/bi-core.ts +43 -0
- package/dist/package/src/bi-dashboard-schema.ts +69 -0
- package/dist/package/src/bi-messages.ts +249 -0
- package/dist/package/src/bi-response.ts +39 -0
- package/dist/package/src/bi-vue.ts +13 -0
- package/dist/package/src/card-schema.ts +111 -0
- package/dist/package/src/components/BiCardChartEditor.vue +139 -0
- package/dist/package/src/components/BiCardFormDialog.vue +289 -0
- package/dist/package/src/components/BiCardLibrary.vue +310 -0
- package/dist/package/src/components/BiCardVisualization.vue +95 -0
- package/dist/package/src/components/BiChart.vue +134 -0
- package/dist/package/src/components/BiDashboard.vue +53 -0
- package/dist/package/src/components/BiDashboardMenuDialog.vue +177 -0
- package/dist/package/src/components/BiDataCard.vue +231 -0
- package/dist/package/src/components/BiDesigner.vue +287 -0
- package/dist/package/src/components/BiSourceModelDialog.vue +846 -0
- package/dist/package/src/components/BiWorkbenchPreview.vue +131 -0
- package/dist/package/src/components/source-model-layout.ts +62 -0
- package/dist/package/src/dataset/index.ts +28 -0
- package/dist/package/src/dataset/labels.ts +19 -0
- package/dist/package/src/dataset/transform.ts +189 -0
- package/dist/package/src/engine/chart-types.ts +44 -0
- package/dist/package/src/engine/echarts.ts +568 -0
- package/dist/package/src/engine/types.ts +60 -0
- package/dist/package/src/env.ts +82 -0
- package/dist/package/src/index.ts +1 -0
- package/dist/package/src/locales/en-US.json +254 -0
- package/dist/package/src/locales/generated.ts +16 -0
- package/dist/package/src/locales/ja-JP.json +254 -0
- package/dist/package/src/locales/zh-CN.json +254 -0
- package/dist/package/src/locales/zh-TW.json +254 -0
- package/dist/package/src/module.ts +12 -0
- package/dist/package/src/rpc/bi/admin/v1/bi_card.ts +169 -0
- package/dist/package/src/rpc/bi/admin/v1/bi_dashboard.ts +126 -0
- package/dist/package/src/rpc/bi/admin/v1/bi_query.ts +187 -0
- package/dist/package/src/rpc/bi/admin/v1/bi_query_record.ts +98 -0
- package/dist/package/src/rpc/bi/admin/v1/bi_source.ts +338 -0
- package/dist/package/src/rpc/common/v1/types.ts +77 -0
- package/dist/package/src/schema/layout.ts +54 -0
- package/dist/package/src/schema/types.ts +103 -0
- package/dist/package/src/styles.scss +1 -0
- package/dist/package/src/theme/index.ts +108 -0
- package/dist/package/src/theme/palettes.ts +34 -0
- package/dist/package/src/theme/presets.ts +278 -0
- package/dist/package/src/views/card/index.vue +5 -0
- package/dist/package/src/views/dashboard/index.vue +58 -0
- package/dist/package/src/views/designer/index.vue +164 -0
- package/dist/package/src/views/manage/index.vue +276 -0
- package/dist/package/src/views/query/index.vue +593 -0
- package/dist/package/src/views/source/index.vue +214 -0
- package/dist/package/src/views/workbench/index.vue +208 -0
- package/package.json +67 -3
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<el-dialog
|
|
3
|
+
v-model="open"
|
|
4
|
+
:title="source?.name || t('bi.source.model.title')"
|
|
5
|
+
width="min(1440px, 96vw)"
|
|
6
|
+
top="3vh"
|
|
7
|
+
destroy-on-close
|
|
8
|
+
class="bi-source-model-dialog"
|
|
9
|
+
>
|
|
10
|
+
<div v-loading="loading" class="source-model">
|
|
11
|
+
<el-steps :active="step" finish-status="success" align-center>
|
|
12
|
+
<el-step :title="t('bi.source.model.step.tables')" />
|
|
13
|
+
<el-step :title="t('bi.source.model.step.relations')" />
|
|
14
|
+
<el-step :title="t('bi.source.model.step.fields')" />
|
|
15
|
+
</el-steps>
|
|
16
|
+
|
|
17
|
+
<section v-if="step === 0" class="source-model__tables">
|
|
18
|
+
<div class="source-model__table-toolbar">
|
|
19
|
+
<el-input v-model="tableFilter" clearable :placeholder="t('bi.source.model.search_tables')" />
|
|
20
|
+
<el-select :model-value="model.base_table" :placeholder="t('bi.source.model.field.base_table')" :disabled="model.relations.length > 0" @update:model-value="setBaseTable(String($event ?? ''))">
|
|
21
|
+
<el-option v-for="table in selectedTables" :key="table.key" :label="schemaLabel(table)" :value="table.key" />
|
|
22
|
+
</el-select>
|
|
23
|
+
</div>
|
|
24
|
+
<el-scrollbar class="source-model__table-list">
|
|
25
|
+
<div v-for="table in filteredTables" :key="table.key" class="source-model__table-row">
|
|
26
|
+
<el-checkbox :model-value="isSelected(table.key)" :disabled="!isSelected(table.key) && model.tables.length >= 8" @change="toggleTable(table, Boolean($event))" />
|
|
27
|
+
<div class="source-model__table-info">
|
|
28
|
+
<strong :title="schemaLabel(table)">{{ schemaLabel(table) }}</strong>
|
|
29
|
+
<small>{{ table.schema_name }} · {{ table.columns.length }} {{ t('bi.source.model.fields_count') }}</small>
|
|
30
|
+
</div>
|
|
31
|
+
<el-select
|
|
32
|
+
v-if="isSelected(table.key)"
|
|
33
|
+
:model-value="modelTable(table.key)?.tenant_column ?? ''"
|
|
34
|
+
clearable
|
|
35
|
+
size="small"
|
|
36
|
+
class="source-model__tenant-select"
|
|
37
|
+
:placeholder="t('bi.source.model.field.shared_table')"
|
|
38
|
+
@update:model-value="setTenantColumn(table.key, String($event ?? ''))"
|
|
39
|
+
>
|
|
40
|
+
<el-option v-for="column in table.columns" :key="column.name" :label="schemaLabel(column)" :value="column.name" />
|
|
41
|
+
</el-select>
|
|
42
|
+
<el-tag size="small" effect="plain">{{ table.table_type }}</el-tag>
|
|
43
|
+
</div>
|
|
44
|
+
<el-empty v-if="!filteredTables.length" :description="t('bi.source.model.tables_empty')" />
|
|
45
|
+
</el-scrollbar>
|
|
46
|
+
<div class="source-model__selection-count">{{ selectedTables.length }} / 8</div>
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section v-else-if="step === 1" class="source-model__relations">
|
|
50
|
+
<VueFlow
|
|
51
|
+
:id="flowId"
|
|
52
|
+
:nodes="nodes"
|
|
53
|
+
:edges="edges"
|
|
54
|
+
:nodes-connectable="selectedTables.length > 1"
|
|
55
|
+
:edges-updatable="false"
|
|
56
|
+
:min-zoom="0.15"
|
|
57
|
+
:max-zoom="1.6"
|
|
58
|
+
@connect="connectTables"
|
|
59
|
+
@edge-click="selectRelation"
|
|
60
|
+
@node-drag-stop="moveTable"
|
|
61
|
+
@pane-click="selectedRelationIndex = -1"
|
|
62
|
+
@init="fitGraph"
|
|
63
|
+
@nodes-initialized="fitGraph"
|
|
64
|
+
>
|
|
65
|
+
<Background :gap="20" :size="1" pattern-color="var(--el-border-color-lighter)" />
|
|
66
|
+
<Controls :show-interactive="false" />
|
|
67
|
+
<MiniMap position="bottom-center" :width="150" :height="90" pannable zoomable node-color="var(--el-color-primary-light-5)" mask-color="var(--el-fill-color-light)" />
|
|
68
|
+
<Panel position="top-right">
|
|
69
|
+
<el-button size="small" @click="layoutGraph">{{ t('bi.source.model.action.auto_layout') }}</el-button>
|
|
70
|
+
</Panel>
|
|
71
|
+
<template #node-table="{ data }">
|
|
72
|
+
<article class="source-model__node" :class="{ 'is-base': data.key === model.base_table }">
|
|
73
|
+
<Handle id="in" type="target" :position="data.targetPosition" :style="data.key === model.base_table ? { top: '12px' } : undefined" />
|
|
74
|
+
<header>
|
|
75
|
+
<strong :title="schemaLabel(tableFor(data.key), data.name)">{{ schemaLabel(tableFor(data.key), data.name) }}</strong>
|
|
76
|
+
<el-tag v-if="data.key === model.base_table" size="small" type="success">{{ t('bi.source.model.field.base_table') }}</el-tag>
|
|
77
|
+
</header>
|
|
78
|
+
<small>{{ data.schema_name }}</small>
|
|
79
|
+
<div class="source-model__node-columns nodrag nowheel">
|
|
80
|
+
<div v-for="column in tableFor(data.key)?.columns" :key="column.name">
|
|
81
|
+
<span :title="schemaLabel(column)">{{ schemaLabel(column) }}</span>
|
|
82
|
+
<small>{{ column.data_type }}</small>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<Handle v-for="handle in data.handles" :id="handle.id" :key="handle.id" type="source" :position="handle.position" :style="{ top: `${handle.top}%` }" />
|
|
86
|
+
</article>
|
|
87
|
+
</template>
|
|
88
|
+
</VueFlow>
|
|
89
|
+
|
|
90
|
+
<aside class="source-model__relation-panel">
|
|
91
|
+
<template v-if="selectedRelation">
|
|
92
|
+
<div class="source-model__relation-heading">
|
|
93
|
+
<strong>{{ t('bi.source.model.field.relation') }}</strong>
|
|
94
|
+
<el-button text type="danger" :icon="Delete" @click="removeRelation" />
|
|
95
|
+
</div>
|
|
96
|
+
<el-form label-position="top">
|
|
97
|
+
<el-form-item :label="schemaLabel(tableFor(selectedRelation.left_table), selectedRelation.left_table)">
|
|
98
|
+
<el-select v-model="selectedRelation.left_column" :placeholder="t('bi.source.model.field.left_column')">
|
|
99
|
+
<el-option v-for="column in tableFor(selectedRelation.left_table)?.columns" :key="column.name" :label="schemaLabel(column)" :value="column.name" />
|
|
100
|
+
</el-select>
|
|
101
|
+
</el-form-item>
|
|
102
|
+
<el-form-item :label="schemaLabel(tableFor(selectedRelation.right_table), selectedRelation.right_table)">
|
|
103
|
+
<el-select v-model="selectedRelation.right_column" :placeholder="t('bi.source.model.field.right_unique_column')">
|
|
104
|
+
<el-option v-for="column in uniqueColumns(selectedRelation.right_table)" :key="column.name" :label="schemaLabel(column)" :value="column.name" />
|
|
105
|
+
</el-select>
|
|
106
|
+
</el-form-item>
|
|
107
|
+
<el-form-item :label="t('bi.source.model.field.join_type')">
|
|
108
|
+
<el-radio-group v-model="selectedRelation.join_type">
|
|
109
|
+
<el-radio-button value="left">LEFT</el-radio-button>
|
|
110
|
+
<el-radio-button value="inner">INNER</el-radio-button>
|
|
111
|
+
</el-radio-group>
|
|
112
|
+
</el-form-item>
|
|
113
|
+
</el-form>
|
|
114
|
+
</template>
|
|
115
|
+
<el-empty v-else :description="t('bi.source.model.relation_empty')" :image-size="72" />
|
|
116
|
+
</aside>
|
|
117
|
+
</section>
|
|
118
|
+
|
|
119
|
+
<section v-else class="source-model__semantics">
|
|
120
|
+
<div class="source-model__semantic-section">
|
|
121
|
+
<header>
|
|
122
|
+
<strong>{{ t('bi.source.model.field.dimensions') }}</strong>
|
|
123
|
+
<el-button size="small" :icon="CirclePlus" @click="addDimension">{{ t('bi.source.model.action.add_dimension') }}</el-button>
|
|
124
|
+
</header>
|
|
125
|
+
<el-table :data="model.dimensions" size="small" border>
|
|
126
|
+
<el-table-column :label="t('bi.source.model.field.code')" min-width="135">
|
|
127
|
+
<template #default="{ row }"><el-input v-model="row.code" /></template>
|
|
128
|
+
</el-table-column>
|
|
129
|
+
<el-table-column :label="t('bi.source.model.field.display_name')" min-width="135">
|
|
130
|
+
<template #default="{ row }"><el-input v-model="row.name" /></template>
|
|
131
|
+
</el-table-column>
|
|
132
|
+
<el-table-column :label="t('bi.source.model.field.table')" min-width="160">
|
|
133
|
+
<template #default="{ row }">
|
|
134
|
+
<el-select v-model="row.table" @change="row.column = ''; row.type = 'string'">
|
|
135
|
+
<el-option v-for="table in selectedTables" :key="table.key" :label="schemaLabel(table)" :value="table.key" />
|
|
136
|
+
</el-select>
|
|
137
|
+
</template>
|
|
138
|
+
</el-table-column>
|
|
139
|
+
<el-table-column :label="t('bi.source.model.field.column')" min-width="160">
|
|
140
|
+
<template #default="{ row }">
|
|
141
|
+
<el-select v-model="row.column" @change="row.type = fieldType(row.table, row.column)">
|
|
142
|
+
<el-option v-for="column in tableFor(row.table)?.columns" :key="column.name" :label="schemaLabel(column)" :value="column.name" />
|
|
143
|
+
</el-select>
|
|
144
|
+
</template>
|
|
145
|
+
</el-table-column>
|
|
146
|
+
<el-table-column :label="t('bi.source.model.field.type')" width="110">
|
|
147
|
+
<template #default="{ row }">{{ row.type }}</template>
|
|
148
|
+
</el-table-column>
|
|
149
|
+
<el-table-column :label="t('bi.source.model.field.filterable')" width="115" align="center">
|
|
150
|
+
<template #default="{ row }"><el-switch v-model="row.filterable" /></template>
|
|
151
|
+
</el-table-column>
|
|
152
|
+
<el-table-column width="64" align="center">
|
|
153
|
+
<template #default="{ $index }"><el-button text type="danger" :icon="Delete" @click="model.dimensions.splice($index, 1)" /></template>
|
|
154
|
+
</el-table-column>
|
|
155
|
+
</el-table>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="source-model__semantic-section">
|
|
159
|
+
<header>
|
|
160
|
+
<strong>{{ t('bi.source.model.field.measures') }}</strong>
|
|
161
|
+
<el-button size="small" :icon="CirclePlus" @click="addMeasure">{{ t('bi.source.model.action.add_measure') }}</el-button>
|
|
162
|
+
</header>
|
|
163
|
+
<el-table :data="model.measures" size="small" border>
|
|
164
|
+
<el-table-column :label="t('bi.source.model.field.code')" min-width="135">
|
|
165
|
+
<template #default="{ row }"><el-input v-model="row.code" /></template>
|
|
166
|
+
</el-table-column>
|
|
167
|
+
<el-table-column :label="t('bi.source.model.field.display_name')" min-width="135">
|
|
168
|
+
<template #default="{ row }"><el-input v-model="row.name" /></template>
|
|
169
|
+
</el-table-column>
|
|
170
|
+
<el-table-column :label="t('bi.source.model.field.column')" min-width="180">
|
|
171
|
+
<template #default="{ row }">
|
|
172
|
+
<el-select v-model="row.column" @change="row.type = 'number'; row.aggregations = defaultAggregations(row.column)">
|
|
173
|
+
<el-option v-for="column in tableFor(model.base_table)?.columns" :key="column.name" :label="schemaLabel(column)" :value="column.name" />
|
|
174
|
+
</el-select>
|
|
175
|
+
</template>
|
|
176
|
+
</el-table-column>
|
|
177
|
+
<el-table-column :label="t('bi.source.model.field.aggregation')" min-width="220">
|
|
178
|
+
<template #default="{ row }">
|
|
179
|
+
<el-select v-model="row.aggregations" multiple collapse-tags>
|
|
180
|
+
<el-option v-for="aggregation in aggregationOptions(row.column)" :key="aggregation" :label="aggregation" :value="aggregation" />
|
|
181
|
+
</el-select>
|
|
182
|
+
</template>
|
|
183
|
+
</el-table-column>
|
|
184
|
+
<el-table-column width="64" align="center">
|
|
185
|
+
<template #default="{ $index }"><el-button text type="danger" :icon="Delete" @click="model.measures.splice($index, 1)" /></template>
|
|
186
|
+
</el-table-column>
|
|
187
|
+
</el-table>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div v-if="previewResult" class="source-model__preview">
|
|
191
|
+
<header>
|
|
192
|
+
<strong>{{ t('bi.query.result') }}</strong>
|
|
193
|
+
<span>{{ t('bi.query.elapsed', {ms: previewResult.elapsed_ms}) }}</span>
|
|
194
|
+
</header>
|
|
195
|
+
<el-table :data="previewRows" size="small" border>
|
|
196
|
+
<el-table-column v-for="(column, index) in previewResult.columns" :key="column.code" :label="column.name" min-width="130">
|
|
197
|
+
<template #default="{ row }">{{ row[`value_${index}`] }}</template>
|
|
198
|
+
</el-table-column>
|
|
199
|
+
</el-table>
|
|
200
|
+
</div>
|
|
201
|
+
</section>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<template #footer>
|
|
205
|
+
<div class="source-model__footer">
|
|
206
|
+
<el-button @click="open = false">{{ t('common.action.cancel') }}</el-button>
|
|
207
|
+
<el-button v-if="step > 0" @click="step--">{{ t('bi.source.model.action.previous') }}</el-button>
|
|
208
|
+
<el-button v-if="step < 2" type="primary" @click="nextStep">{{ t('bi.source.model.action.next') }}</el-button>
|
|
209
|
+
<template v-else>
|
|
210
|
+
<el-button type="primary" :loading="saving" @click="saveModel">{{ t('bi.source.model.action.save') }}</el-button>
|
|
211
|
+
<el-button :icon="DataAnalysis" :disabled="!modelSaved" :loading="previewing" @click="previewModel">{{ t('bi.source.model.action.preview') }}</el-button>
|
|
212
|
+
<el-button v-if="modelSaved" @click="open = false">{{ t('bi.source.model.action.done') }}</el-button>
|
|
213
|
+
</template>
|
|
214
|
+
</div>
|
|
215
|
+
</template>
|
|
216
|
+
</el-dialog>
|
|
217
|
+
</template>
|
|
218
|
+
|
|
219
|
+
<script setup lang="ts">
|
|
220
|
+
import { computed, nextTick, ref, useId, watch } from "vue";
|
|
221
|
+
import { VueFlow, Handle, MarkerType, Panel, Position, useVueFlow } from "@vue-flow/core";
|
|
222
|
+
import type { Connection, Edge, EdgeMouseEvent, Node, NodeDragEvent } from "@vue-flow/core";
|
|
223
|
+
import { Background } from "@vue-flow/background";
|
|
224
|
+
import { Controls } from "@vue-flow/controls";
|
|
225
|
+
import { MiniMap } from "@vue-flow/minimap";
|
|
226
|
+
import { CirclePlus, DataAnalysis, Delete } from "@element-plus/icons-vue";
|
|
227
|
+
import { t } from "@liujitcn/kratos-admin-core";
|
|
228
|
+
import { defBiQueryService } from "../api/bi/admin/v1/bi_query";
|
|
229
|
+
import { defBiSourceService } from "../api/bi/admin/v1/bi_source";
|
|
230
|
+
import { formatSourceModelLayout } from "./source-model-layout";
|
|
231
|
+
import type { BiQueryResult } from "../rpc/bi/admin/v1/bi_query";
|
|
232
|
+
import type {
|
|
233
|
+
BiSource,
|
|
234
|
+
BiSourceDimension,
|
|
235
|
+
BiSourceMeasure,
|
|
236
|
+
BiSourceModel,
|
|
237
|
+
BiSourceModelTable,
|
|
238
|
+
BiSourceRelation,
|
|
239
|
+
BiSourceSchemaColumn,
|
|
240
|
+
BiSourceSchemaTable
|
|
241
|
+
} from "../rpc/bi/admin/v1/bi_source";
|
|
242
|
+
import "@vue-flow/core/dist/style.css";
|
|
243
|
+
import "@vue-flow/controls/dist/style.css";
|
|
244
|
+
import "@vue-flow/minimap/dist/style.css";
|
|
245
|
+
|
|
246
|
+
/** 数据库中保存的语义模型 JSON。 */
|
|
247
|
+
interface StoredSourceModel {
|
|
248
|
+
/** 事实表标识。 */
|
|
249
|
+
baseTable?: string;
|
|
250
|
+
/** 已选数据表。 */
|
|
251
|
+
tables?: Array<BiSourceModelTable & {tenantColumn?: string}>;
|
|
252
|
+
/** 表关系。 */
|
|
253
|
+
relations?: Array<BiSourceRelation & {leftTable?: string; leftColumn?: string; rightTable?: string; rightColumn?: string; joinType?: string}>;
|
|
254
|
+
/** 语义维度。 */
|
|
255
|
+
dimensions?: Array<BiSourceDimension & {filterable?: boolean}>;
|
|
256
|
+
/** 语义指标。 */
|
|
257
|
+
measures?: Array<BiSourceMeasure & {aggregations?: string[]}>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const props = defineProps<{modelValue: boolean; source?: BiSource}>();
|
|
261
|
+
const emit = defineEmits<{"update:modelValue": [value: boolean]; saved: [source: BiSource]}>();
|
|
262
|
+
const open = computed({get: () => props.modelValue, set: value => emit("update:modelValue", value)});
|
|
263
|
+
const flowId = `bi-source-model-${useId()}`;
|
|
264
|
+
const { fitView, updateNodeInternals, viewportHelper } = useVueFlow(flowId);
|
|
265
|
+
const loading = ref(false);
|
|
266
|
+
const saving = ref(false);
|
|
267
|
+
const previewing = ref(false);
|
|
268
|
+
const step = ref(0);
|
|
269
|
+
const tableFilter = ref("");
|
|
270
|
+
const schemaTables = ref<BiSourceSchemaTable[]>([]);
|
|
271
|
+
const model = ref<BiSourceModel>(emptyModel());
|
|
272
|
+
const selectedRelationIndex = ref(-1);
|
|
273
|
+
const savedModelSnapshot = ref("");
|
|
274
|
+
const modelSaved = computed(() => savedModelSnapshot.value !== "" && savedModelSnapshot.value === JSON.stringify(model.value));
|
|
275
|
+
const previewResult = ref<BiQueryResult>();
|
|
276
|
+
const selectedTables = computed(() => schemaTables.value.filter(table => model.value.tables.some(item => item.key === table.key)));
|
|
277
|
+
const filteredTables = computed(() => schemaTables.value.filter(table => `${table.schema_name}.${schemaLabel(table)}`.toLowerCase().includes(tableFilter.value.toLowerCase())));
|
|
278
|
+
const selectedRelation = computed(() => model.value.relations[selectedRelationIndex.value]);
|
|
279
|
+
/** 数据表节点及按目标纵向顺序分布的连接点。 */
|
|
280
|
+
type TableNodeData = {key: string; name: string; schema_name: string; targetPosition: Position; handles: Array<{id: string; top: number; position: Position}>};
|
|
281
|
+
const nodes = computed<Node<TableNodeData>[]>(() => selectedTables.value.map((table, index) => {
|
|
282
|
+
const saved = model.value.tables.find(item => item.key === table.key);
|
|
283
|
+
const incoming = model.value.relations.find(relation => relation.right_table === table.key);
|
|
284
|
+
const targetPosition = incoming && (modelTable(incoming.left_table)?.x ?? 0) > (saved?.x ?? 0) ? Position.Right : Position.Left;
|
|
285
|
+
const outgoing = model.value.relations.map((relation, relationIndex) => ({
|
|
286
|
+
relation,
|
|
287
|
+
relationIndex,
|
|
288
|
+
position: (modelTable(relation.right_table)?.x ?? 0) < (saved?.x ?? 0) ? Position.Left : Position.Right
|
|
289
|
+
})).filter(({relation}) => relation.left_table === table.key)
|
|
290
|
+
.sort((left, right) => (modelTable(left.relation.right_table)?.y ?? 0) - (modelTable(right.relation.right_table)?.y ?? 0));
|
|
291
|
+
return {
|
|
292
|
+
id: table.key,
|
|
293
|
+
type: "table",
|
|
294
|
+
position: {x: saved?.x ?? 80 + (index % 3) * 330, y: saved?.y ?? 60 + Math.floor(index / 3) * 290},
|
|
295
|
+
data: {
|
|
296
|
+
key: table.key,
|
|
297
|
+
name: table.name,
|
|
298
|
+
schema_name: table.schema_name,
|
|
299
|
+
targetPosition,
|
|
300
|
+
handles: outgoing.length ? outgoing.map(handle => {
|
|
301
|
+
const siblings = outgoing.filter(item => item.position === handle.position);
|
|
302
|
+
return {id: `relation-${handle.relationIndex}`, top: (siblings.indexOf(handle) + 1) * 100 / (siblings.length + 1), position: handle.position};
|
|
303
|
+
}) : [{id: "out", top: 50, position: targetPosition === Position.Right ? Position.Left : Position.Right}]
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}));
|
|
307
|
+
const edges = computed<Edge[]>(() => model.value.relations.map((relation, index) => ({
|
|
308
|
+
id: `relation-${index}`,
|
|
309
|
+
source: relation.left_table,
|
|
310
|
+
target: relation.right_table,
|
|
311
|
+
sourceHandle: `relation-${index}`,
|
|
312
|
+
targetHandle: "in",
|
|
313
|
+
type: "default",
|
|
314
|
+
selected: selectedRelationIndex.value === index,
|
|
315
|
+
label: `${schemaLabel(tableFor(relation.left_table)?.columns.find(column => column.name === relation.left_column), relation.left_column || "?")} → ${schemaLabel(tableFor(relation.right_table)?.columns.find(column => column.name === relation.right_column), relation.right_column || "?")}`,
|
|
316
|
+
markerEnd: MarkerType.ArrowClosed,
|
|
317
|
+
interactionWidth: 20,
|
|
318
|
+
style: {stroke: selectedRelationIndex.value === index ? "var(--el-color-primary)" : "var(--el-text-color-placeholder)"},
|
|
319
|
+
labelStyle: {fill: "var(--el-text-color-regular)", fontSize: 12},
|
|
320
|
+
labelBgStyle: {fill: "var(--el-bg-color)", stroke: "var(--el-border-color-lighter)"},
|
|
321
|
+
labelBgPadding: [6, 4],
|
|
322
|
+
labelBgBorderRadius: 4
|
|
323
|
+
})));
|
|
324
|
+
const previewRows = computed(() => previewResult.value?.rows.map(row => Object.fromEntries(row.values.map((value, index) => [`value_${index}`, value]))) ?? []);
|
|
325
|
+
|
|
326
|
+
watch(() => props.modelValue, visible => {
|
|
327
|
+
if (visible) void loadModel();
|
|
328
|
+
});
|
|
329
|
+
watch(model, () => { previewResult.value = undefined; }, {deep: true});
|
|
330
|
+
watch(nodes, async () => {
|
|
331
|
+
await nextTick();
|
|
332
|
+
updateNodeInternals();
|
|
333
|
+
}, {flush: "post"});
|
|
334
|
+
|
|
335
|
+
/** 初始化数据库结构和已保存的语义模型。 */
|
|
336
|
+
async function loadModel() {
|
|
337
|
+
if (!props.source) return;
|
|
338
|
+
loading.value = true;
|
|
339
|
+
step.value = 0;
|
|
340
|
+
savedModelSnapshot.value = "";
|
|
341
|
+
previewResult.value = undefined;
|
|
342
|
+
try {
|
|
343
|
+
const result = await defBiSourceService.ListBiSourceTables({id: props.source.id});
|
|
344
|
+
schemaTables.value = result.tables ?? [];
|
|
345
|
+
model.value = readStoredModel(props.source.schema_json);
|
|
346
|
+
if (!model.value.tables.length) model.value = emptyModel();
|
|
347
|
+
savedModelSnapshot.value = props.source.model_configured ? JSON.stringify(model.value) : "";
|
|
348
|
+
} catch (cause) {
|
|
349
|
+
ElMessage.error(cause instanceof Error ? cause.message : t("bi.query.error"));
|
|
350
|
+
} finally {
|
|
351
|
+
loading.value = false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** 将数据库中保存的 JSON 转换为前端模型。 */
|
|
356
|
+
function readStoredModel(value: string): BiSourceModel {
|
|
357
|
+
try {
|
|
358
|
+
const stored = JSON.parse(value) as StoredSourceModel;
|
|
359
|
+
return {
|
|
360
|
+
base_table: stored.baseTable ?? "",
|
|
361
|
+
tables: (stored.tables ?? []).map(table => ({key: table.key, tenant_column: table.tenantColumn ?? table.tenant_column ?? "", x: table.x ?? 0, y: table.y ?? 0})),
|
|
362
|
+
relations: (stored.relations ?? []).map(relation => ({
|
|
363
|
+
left_table: relation.leftTable ?? relation.left_table ?? "",
|
|
364
|
+
left_column: relation.leftColumn ?? relation.left_column ?? "",
|
|
365
|
+
right_table: relation.rightTable ?? relation.right_table ?? "",
|
|
366
|
+
right_column: relation.rightColumn ?? relation.right_column ?? "",
|
|
367
|
+
join_type: relation.joinType ?? relation.join_type ?? "left"
|
|
368
|
+
})),
|
|
369
|
+
dimensions: (stored.dimensions ?? []).map(dimension => ({...dimension, filterable: dimension.filterable ?? false})),
|
|
370
|
+
measures: (stored.measures ?? []).map(measure => ({...measure, aggregations: measure.aggregations ?? []}))
|
|
371
|
+
};
|
|
372
|
+
} catch {
|
|
373
|
+
return emptyModel();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 创建空语义模型。 */
|
|
378
|
+
function emptyModel(): BiSourceModel {
|
|
379
|
+
return {base_table: "", tables: [], relations: [], dimensions: [], measures: []};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** 判断表是否已加入模型。 */
|
|
383
|
+
function isSelected(key: string): boolean {
|
|
384
|
+
return model.value.tables.some(table => table.key === key);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** 更换事实表时仅保留仍属于新事实表的指标。 */
|
|
388
|
+
function setBaseTable(key: string) {
|
|
389
|
+
model.value.base_table = key;
|
|
390
|
+
model.value.measures = model.value.measures.filter(measure => measure.table === key);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** 查询模型表节点以编辑租户隔离字段。 */
|
|
394
|
+
function modelTable(key: string): BiSourceModelTable | undefined {
|
|
395
|
+
return model.value.tables.find(table => table.key === key);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 更新表的租户隔离字段。 */
|
|
399
|
+
function setTenantColumn(key: string, column: string) {
|
|
400
|
+
const table = modelTable(key);
|
|
401
|
+
if (table) table.tenant_column = column;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** 加入或移除建模表并清理失效的关系和字段。 */
|
|
405
|
+
function toggleTable(table: BiSourceSchemaTable, selected: boolean) {
|
|
406
|
+
if (selected) {
|
|
407
|
+
if (model.value.tables.length >= 8) return;
|
|
408
|
+
const tenantColumn = table.columns.find(column => column.name === "tenant_id")?.name ?? "";
|
|
409
|
+
model.value.tables.push({key: table.key, tenant_column: tenantColumn, x: 80 + (model.value.tables.length % 3) * 330, y: 60 + Math.floor(model.value.tables.length / 3) * 290});
|
|
410
|
+
if (!model.value.base_table) model.value.base_table = table.key;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
model.value.tables = model.value.tables.filter(item => item.key !== table.key);
|
|
414
|
+
model.value.relations = model.value.relations.filter(item => item.left_table !== table.key && item.right_table !== table.key);
|
|
415
|
+
model.value.dimensions = model.value.dimensions.filter(item => item.table !== table.key);
|
|
416
|
+
model.value.measures = model.value.measures.filter(item => item.table !== table.key);
|
|
417
|
+
if (model.value.base_table === table.key) model.value.base_table = model.value.tables[0]?.key ?? "";
|
|
418
|
+
selectedRelationIndex.value = -1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** 查询指定表的发现结构。 */
|
|
422
|
+
function tableFor(key: string): BiSourceSchemaTable | undefined {
|
|
423
|
+
return schemaTables.value.find(table => table.key === key);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** 表和字段展示注释,选择值仍使用原始英文标识。 */
|
|
427
|
+
function schemaLabel(item: Pick<BiSourceSchemaColumn, "name" | "comment"> | undefined, name = item?.name ?? ""): string {
|
|
428
|
+
return item?.comment ? `${name}(${item.comment})` : name;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** 返回维度表中可作为多对一目标的唯一字段。 */
|
|
432
|
+
function uniqueColumns(key: string): BiSourceSchemaColumn[] {
|
|
433
|
+
return tableFor(key)?.columns.filter(column => column.unique) ?? [];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** 按数据库字段类型推导 BI 展示类型。 */
|
|
437
|
+
function fieldType(table: string, column: string): string {
|
|
438
|
+
const value = tableFor(table)?.columns.find(item => item.name === column)?.data_type.toLowerCase() ?? "";
|
|
439
|
+
if (/int|decimal|numeric|real|double|float/.test(value)) return "number";
|
|
440
|
+
if (/date|time/.test(value)) return "date";
|
|
441
|
+
return "string";
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** 返回当前度量字段可用的聚合函数。 */
|
|
445
|
+
function aggregationOptions(column: string): string[] {
|
|
446
|
+
return fieldType(model.value.base_table, column) === "number" ? ["count", "sum", "avg", "min", "max"] : ["count"];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** 为新增度量选择默认聚合方式。 */
|
|
450
|
+
function defaultAggregations(column: string): string[] {
|
|
451
|
+
return fieldType(model.value.base_table, column) === "number" ? ["sum"] : ["count"];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** 增加一条语义维度。 */
|
|
455
|
+
function addDimension() {
|
|
456
|
+
const table = selectedTables.value[0];
|
|
457
|
+
const column = table?.columns[0];
|
|
458
|
+
if (!table || !column) return;
|
|
459
|
+
const code = semanticCode(table.name, column.name, model.value.dimensions.length);
|
|
460
|
+
model.value.dimensions.push({code, name: column.name, type: fieldType(table.key, column.name), table: table.key, column: column.name, filterable: true});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** 增加一条事实表指标。 */
|
|
464
|
+
function addMeasure() {
|
|
465
|
+
const table = tableFor(model.value.base_table);
|
|
466
|
+
const column = table?.columns.find(item => fieldType(table.key, item.name) === "number") ?? table?.columns[0];
|
|
467
|
+
if (!table || !column) return;
|
|
468
|
+
const code = semanticCode(table.name, column.name, model.value.measures.length);
|
|
469
|
+
model.value.measures.push({code, name: column.name, type: "number", table: table.key, column: column.name, aggregations: defaultAggregations(column.name)});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** 生成合法且稳定的语义字段编码。 */
|
|
473
|
+
function semanticCode(table: string, column: string, index: number): string {
|
|
474
|
+
const prefix = `${table}_${column}`.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
475
|
+
let suffix = index + 1;
|
|
476
|
+
let code = `${prefix}_${suffix}`;
|
|
477
|
+
while ([...model.value.dimensions, ...model.value.measures].some(field => field.code === code)) {
|
|
478
|
+
code = `${prefix}_${++suffix}`;
|
|
479
|
+
}
|
|
480
|
+
return code;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** 拖动画布连接点后创建一条待配置关系。 */
|
|
484
|
+
function connectTables(connection: Connection) {
|
|
485
|
+
const source = connection.source;
|
|
486
|
+
const target = connection.target;
|
|
487
|
+
if (!source || !target || source === target || model.value.relations.some(item => item.right_table === target)) return;
|
|
488
|
+
let leftTable = source;
|
|
489
|
+
let rightTable = target;
|
|
490
|
+
const sourceReachable = reachableFromBase(source);
|
|
491
|
+
const targetReachable = reachableFromBase(target);
|
|
492
|
+
if (targetReachable && !sourceReachable) [leftTable, rightTable] = [target, source];
|
|
493
|
+
else if (sourceReachable && targetReachable) return;
|
|
494
|
+
model.value.relations.push({left_table: leftTable, left_column: "", right_table: rightTable, right_column: "", join_type: "left"});
|
|
495
|
+
selectedRelationIndex.value = model.value.relations.length - 1;
|
|
496
|
+
void layoutGraph();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** 判断表是否已经从事实表连通。 */
|
|
500
|
+
function reachableFromBase(key: string): boolean {
|
|
501
|
+
const reachable = new Set([model.value.base_table]);
|
|
502
|
+
for (let changed = true; changed;) {
|
|
503
|
+
changed = false;
|
|
504
|
+
for (const relation of model.value.relations) {
|
|
505
|
+
if (reachable.has(relation.left_table) && !reachable.has(relation.right_table)) {
|
|
506
|
+
reachable.add(relation.right_table);
|
|
507
|
+
changed = true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return reachable.has(key);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** 选择画布关系并打开其配置。 */
|
|
515
|
+
function selectRelation({edge}: EdgeMouseEvent) {
|
|
516
|
+
selectedRelationIndex.value = Number(edge.id.replace("relation-", ""));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** 删除当前选中的关系。 */
|
|
520
|
+
function removeRelation() {
|
|
521
|
+
if (selectedRelationIndex.value < 0) return;
|
|
522
|
+
model.value.relations.splice(selectedRelationIndex.value, 1);
|
|
523
|
+
selectedRelationIndex.value = -1;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** 将画布节点坐标保存到语义模型。 */
|
|
527
|
+
function moveTable({node}: NodeDragEvent) {
|
|
528
|
+
const table = model.value.tables.find(item => item.key === node.id);
|
|
529
|
+
if (!table) return;
|
|
530
|
+
table.x = Math.round(node.position.x);
|
|
531
|
+
table.y = Math.round(node.position.y);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** 整理表关系布局后适配画布视口。 */
|
|
535
|
+
async function layoutGraph() {
|
|
536
|
+
formatSourceModelLayout(model.value);
|
|
537
|
+
await nextTick();
|
|
538
|
+
fitGraph();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** 初始化或恢复画布视口。 */
|
|
542
|
+
function fitGraph() {
|
|
543
|
+
if (viewportHelper.value.viewportInitialized) void fitView({padding: 0.18, maxZoom: 1});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** 校验当前步骤后进入下一步。 */
|
|
547
|
+
function nextStep() {
|
|
548
|
+
if (step.value === 0 && (!model.value.base_table || selectedTables.value.length === 0)) {
|
|
549
|
+
ElMessage.warning(t("bi.source.model.validation.select_tables"));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (step.value === 0) formatSourceModelLayout(model.value);
|
|
553
|
+
step.value++;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** 保存关系模型并启用数据源。 */
|
|
557
|
+
async function saveModel() {
|
|
558
|
+
if (!props.source) return;
|
|
559
|
+
saving.value = true;
|
|
560
|
+
try {
|
|
561
|
+
const result = await defBiSourceService.SaveBiSourceModel({id: props.source.id, model: model.value});
|
|
562
|
+
savedModelSnapshot.value = JSON.stringify(model.value);
|
|
563
|
+
previewResult.value = undefined;
|
|
564
|
+
emit("saved", result);
|
|
565
|
+
ElMessage.success(t("bi.source.model.message.saved"));
|
|
566
|
+
} catch (cause) {
|
|
567
|
+
ElMessage.error(cause instanceof Error ? cause.message : t("bi.query.error"));
|
|
568
|
+
} finally {
|
|
569
|
+
saving.value = false;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** 使用一个维度和指标预览已保存模型。 */
|
|
574
|
+
async function previewModel() {
|
|
575
|
+
if (!props.source || !modelSaved.value || !model.value.measures.length) return;
|
|
576
|
+
previewing.value = true;
|
|
577
|
+
try {
|
|
578
|
+
const dimension = model.value.dimensions[0];
|
|
579
|
+
const measure = model.value.measures[0];
|
|
580
|
+
previewResult.value = await defBiQueryService.PreviewBiQuery({query: {
|
|
581
|
+
source_code: props.source.code,
|
|
582
|
+
dimensions: dimension ? [dimension.code] : [],
|
|
583
|
+
measures: [{code: measure.code, aggregation: measure.aggregations[0] ?? "count"}],
|
|
584
|
+
filters: [],
|
|
585
|
+
limit: 10,
|
|
586
|
+
time_range: undefined,
|
|
587
|
+
orders: []
|
|
588
|
+
}});
|
|
589
|
+
} catch (cause) {
|
|
590
|
+
ElMessage.error(cause instanceof Error ? cause.message : t("bi.query.error"));
|
|
591
|
+
} finally {
|
|
592
|
+
previewing.value = false;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
</script>
|
|
596
|
+
|
|
597
|
+
<style scoped lang="scss">
|
|
598
|
+
.source-model {
|
|
599
|
+
min-height: 560px;
|
|
600
|
+
padding-top: 4px;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.source-model__tables {
|
|
604
|
+
display: grid;
|
|
605
|
+
grid-template-rows: auto 1fr auto;
|
|
606
|
+
height: 510px;
|
|
607
|
+
margin-top: 20px;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.source-model__table-toolbar {
|
|
611
|
+
display: grid;
|
|
612
|
+
grid-template-columns: minmax(220px, 1fr) minmax(220px, 320px);
|
|
613
|
+
gap: 12px;
|
|
614
|
+
padding-bottom: 12px;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.source-model__table-list {
|
|
618
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
619
|
+
border-radius: var(--admin-page-radius, 6px);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.source-model__table-row {
|
|
623
|
+
display: flex;
|
|
624
|
+
min-height: 52px;
|
|
625
|
+
align-items: center;
|
|
626
|
+
gap: 12px;
|
|
627
|
+
padding: 8px 14px;
|
|
628
|
+
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.source-model__table-info {
|
|
632
|
+
display: flex;
|
|
633
|
+
min-width: 0;
|
|
634
|
+
flex: 1;
|
|
635
|
+
flex-direction: column;
|
|
636
|
+
gap: 3px;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.source-model__table-info strong,
|
|
640
|
+
.source-model__table-info small {
|
|
641
|
+
overflow: hidden;
|
|
642
|
+
text-overflow: ellipsis;
|
|
643
|
+
white-space: nowrap;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.source-model__table-info small,
|
|
647
|
+
.source-model__selection-count {
|
|
648
|
+
color: var(--el-text-color-secondary);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.source-model__tenant-select {
|
|
652
|
+
width: 190px;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.source-model__selection-count {
|
|
656
|
+
padding-top: 8px;
|
|
657
|
+
text-align: right;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.source-model__relations {
|
|
661
|
+
display: grid;
|
|
662
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
663
|
+
gap: 12px;
|
|
664
|
+
height: 510px;
|
|
665
|
+
margin-top: 20px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.source-model__relations :deep(.vue-flow) {
|
|
669
|
+
overflow: hidden;
|
|
670
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
671
|
+
border-radius: var(--admin-page-radius, 6px);
|
|
672
|
+
background: var(--el-fill-color-lighter);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.source-model__node {
|
|
676
|
+
display: flex;
|
|
677
|
+
width: 250px;
|
|
678
|
+
height: 230px;
|
|
679
|
+
box-sizing: border-box;
|
|
680
|
+
flex-direction: column;
|
|
681
|
+
padding: 10px;
|
|
682
|
+
color: var(--el-text-color-primary);
|
|
683
|
+
background: var(--el-bg-color);
|
|
684
|
+
border: 1px solid var(--el-border-color);
|
|
685
|
+
border-radius: var(--admin-page-radius, 6px);
|
|
686
|
+
box-shadow: var(--el-box-shadow-light);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.source-model__node.is-base {
|
|
690
|
+
border-color: var(--el-color-success);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.source-model__node header,
|
|
694
|
+
.source-model__relation-heading,
|
|
695
|
+
.source-model__semantic-section header,
|
|
696
|
+
.source-model__preview header {
|
|
697
|
+
display: flex;
|
|
698
|
+
align-items: center;
|
|
699
|
+
justify-content: space-between;
|
|
700
|
+
gap: 8px;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.source-model__node header strong {
|
|
704
|
+
min-width: 0;
|
|
705
|
+
overflow: hidden;
|
|
706
|
+
text-overflow: ellipsis;
|
|
707
|
+
white-space: nowrap;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.source-model__node header .el-tag {
|
|
711
|
+
flex-shrink: 0;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.source-model__node > small {
|
|
715
|
+
color: var(--el-text-color-secondary);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.source-model__node-columns {
|
|
719
|
+
min-height: 0;
|
|
720
|
+
flex: 1;
|
|
721
|
+
overflow: auto;
|
|
722
|
+
margin-top: 8px;
|
|
723
|
+
border-top: 1px solid var(--el-border-color-lighter);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.source-model__node-columns > div {
|
|
727
|
+
display: flex;
|
|
728
|
+
justify-content: space-between;
|
|
729
|
+
gap: 8px;
|
|
730
|
+
padding: 4px 2px;
|
|
731
|
+
font-size: 12px;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.source-model__node-columns small {
|
|
735
|
+
color: var(--el-text-color-secondary);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.source-model__node-columns span {
|
|
739
|
+
min-width: 0;
|
|
740
|
+
overflow: hidden;
|
|
741
|
+
text-overflow: ellipsis;
|
|
742
|
+
white-space: nowrap;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.source-model__relations :deep(.vue-flow__edge-path) {
|
|
746
|
+
stroke-width: 1.5;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.source-model__relations :deep(.vue-flow__edge.selected .vue-flow__edge-path) {
|
|
750
|
+
stroke-width: 2.5;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.source-model__relations :deep(.vue-flow__handle) {
|
|
754
|
+
width: 8px;
|
|
755
|
+
height: 8px;
|
|
756
|
+
background: var(--el-color-primary);
|
|
757
|
+
border: 2px solid var(--el-bg-color);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.source-model__relations :deep(.vue-flow__controls),
|
|
761
|
+
.source-model__relations :deep(.vue-flow__minimap) {
|
|
762
|
+
background: var(--el-bg-color);
|
|
763
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
764
|
+
border-radius: var(--admin-page-radius, 6px);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.source-model__relations :deep(.vue-flow__controls-button) {
|
|
768
|
+
color: var(--el-text-color-primary);
|
|
769
|
+
fill: currentColor;
|
|
770
|
+
background: var(--el-bg-color);
|
|
771
|
+
border-color: var(--el-border-color-lighter);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.source-model__relation-panel {
|
|
775
|
+
min-width: 0;
|
|
776
|
+
padding: 14px;
|
|
777
|
+
border: 1px solid var(--el-border-color-lighter);
|
|
778
|
+
border-radius: var(--admin-page-radius, 6px);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.source-model__relation-heading {
|
|
782
|
+
margin-bottom: 14px;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.source-model__semantics {
|
|
786
|
+
display: grid;
|
|
787
|
+
gap: 14px;
|
|
788
|
+
max-height: 510px;
|
|
789
|
+
overflow: auto;
|
|
790
|
+
margin-top: 20px;
|
|
791
|
+
padding-right: 2px;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.source-model__semantic-section,
|
|
795
|
+
.source-model__preview {
|
|
796
|
+
display: grid;
|
|
797
|
+
gap: 10px;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.source-model__semantic-section header strong,
|
|
801
|
+
.source-model__preview header strong {
|
|
802
|
+
font-weight: 600;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.source-model__preview header span {
|
|
806
|
+
color: var(--el-text-color-secondary);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.source-model__footer {
|
|
810
|
+
display: flex;
|
|
811
|
+
flex-wrap: wrap;
|
|
812
|
+
justify-content: flex-end;
|
|
813
|
+
gap: 8px;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
@media (max-width: 760px) {
|
|
817
|
+
.source-model__table-toolbar,
|
|
818
|
+
.source-model__relations {
|
|
819
|
+
grid-template-columns: 1fr;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.source-model__table-row {
|
|
823
|
+
flex-wrap: wrap;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.source-model__table-info {
|
|
827
|
+
flex-basis: calc(100% - 44px);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.source-model__relations {
|
|
831
|
+
height: auto;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.source-model__tenant-select {
|
|
835
|
+
width: 130px;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.source-model__relations :deep(.vue-flow) {
|
|
839
|
+
height: 420px;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.source-model__relation-panel {
|
|
843
|
+
min-height: 200px;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
</style>
|