@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.
Files changed (166) hide show
  1. package/README.md +43 -2
  2. package/dist/package/declarations/index.d.ts +1 -0
  3. package/dist/package/declarations/src/api/bi/admin/v1/bi_card.d.ts +18 -0
  4. package/dist/package/declarations/src/api/bi/admin/v1/bi_card.js +46 -0
  5. package/dist/package/declarations/src/api/bi/admin/v1/bi_dashboard.d.ts +17 -0
  6. package/dist/package/declarations/src/api/bi/admin/v1/bi_dashboard.js +30 -0
  7. package/dist/package/declarations/src/api/bi/admin/v1/bi_query.d.ts +11 -0
  8. package/dist/package/declarations/src/api/bi/admin/v1/bi_query.js +41 -0
  9. package/dist/package/declarations/src/api/bi/admin/v1/bi_query_record.d.ts +10 -0
  10. package/dist/package/declarations/src/api/bi/admin/v1/bi_query_record.js +14 -0
  11. package/dist/package/declarations/src/api/bi/admin/v1/bi_source.d.ts +26 -0
  12. package/dist/package/declarations/src/api/bi/admin/v1/bi_source.js +46 -0
  13. package/dist/package/declarations/src/bi-core.d.ts +17 -0
  14. package/dist/package/declarations/src/bi-core.js +22 -0
  15. package/dist/package/declarations/src/bi-dashboard-schema.d.ts +31 -0
  16. package/dist/package/declarations/src/bi-dashboard-schema.js +39 -0
  17. package/dist/package/declarations/src/bi-messages.d.ts +247 -0
  18. package/dist/package/declarations/src/bi-messages.js +247 -0
  19. package/dist/package/declarations/src/bi-response.d.ts +10 -0
  20. package/dist/package/declarations/src/bi-response.js +33 -0
  21. package/dist/package/declarations/src/bi-vue.d.ts +10 -0
  22. package/dist/package/declarations/src/bi-vue.js +10 -0
  23. package/dist/package/declarations/src/card-schema.d.ts +30 -0
  24. package/dist/package/declarations/src/card-schema.js +87 -0
  25. package/dist/package/declarations/src/components/BiCardChartEditor.vue.d.ts +17 -0
  26. package/dist/package/declarations/src/components/BiCardChartEditor.vue.js +527 -0
  27. package/dist/package/declarations/src/components/BiCardFormDialog.vue.d.ts +20 -0
  28. package/dist/package/declarations/src/components/BiCardFormDialog.vue.js +495 -0
  29. package/dist/package/declarations/src/components/BiCardLibrary.vue.d.ts +3 -0
  30. package/dist/package/declarations/src/components/BiCardLibrary.vue.js +807 -0
  31. package/dist/package/declarations/src/components/BiCardVisualization.vue.d.ts +10 -0
  32. package/dist/package/declarations/src/components/BiCardVisualization.vue.js +97 -0
  33. package/dist/package/declarations/src/components/BiChart.vue.d.ts +20 -0
  34. package/dist/package/declarations/src/components/BiChart.vue.js +112 -0
  35. package/dist/package/declarations/src/components/BiDashboard.vue.d.ts +9 -0
  36. package/dist/package/declarations/src/components/BiDashboard.vue.js +81 -0
  37. package/dist/package/declarations/src/components/BiDashboardMenuDialog.vue.d.ts +12 -0
  38. package/dist/package/declarations/src/components/BiDashboardMenuDialog.vue.js +252 -0
  39. package/dist/package/declarations/src/components/BiDataCard.vue.d.ts +36 -0
  40. package/dist/package/declarations/src/components/BiDataCard.vue.js +372 -0
  41. package/dist/package/declarations/src/components/BiDesigner.vue.d.ts +17 -0
  42. package/dist/package/declarations/src/components/BiDesigner.vue.js +666 -0
  43. package/dist/package/declarations/src/components/BiSourceModelDialog.vue.d.ts +17 -0
  44. package/dist/package/declarations/src/components/BiSourceModelDialog.vue.js +2118 -0
  45. package/dist/package/declarations/src/components/BiWorkbenchPreview.vue.d.ts +7 -0
  46. package/dist/package/declarations/src/components/BiWorkbenchPreview.vue.js +176 -0
  47. package/dist/package/declarations/src/components/source-model-layout.d.ts +3 -0
  48. package/dist/package/declarations/src/components/source-model-layout.js +66 -0
  49. package/dist/package/declarations/src/dataset/index.d.ts +7 -0
  50. package/dist/package/declarations/src/dataset/index.js +25 -0
  51. package/dist/package/declarations/src/dataset/labels.d.ts +2 -0
  52. package/dist/package/declarations/src/dataset/labels.js +21 -0
  53. package/dist/package/declarations/src/dataset/transform.d.ts +20 -0
  54. package/dist/package/declarations/src/dataset/transform.js +173 -0
  55. package/dist/package/declarations/src/engine/chart-types.d.ts +14 -0
  56. package/dist/package/declarations/src/engine/chart-types.js +37 -0
  57. package/dist/package/declarations/src/engine/echarts.d.ts +16 -0
  58. package/dist/package/declarations/src/engine/echarts.js +491 -0
  59. package/dist/package/declarations/src/engine/types.d.ts +36 -0
  60. package/dist/package/declarations/src/engine/types.js +20 -0
  61. package/dist/package/declarations/src/env.d.ts +17 -0
  62. package/dist/package/declarations/src/env.js +67 -0
  63. package/dist/package/declarations/src/index.d.ts +1 -0
  64. package/dist/package/declarations/src/index.js +1 -0
  65. package/dist/package/declarations/src/locales/en-US.json +254 -0
  66. package/dist/package/declarations/src/locales/generated.d.ts +1021 -0
  67. package/dist/package/declarations/src/locales/generated.js +13 -0
  68. package/dist/package/declarations/src/locales/ja-JP.json +254 -0
  69. package/dist/package/declarations/src/locales/zh-CN.json +254 -0
  70. package/dist/package/declarations/src/locales/zh-TW.json +254 -0
  71. package/dist/package/declarations/src/module.d.ts +2 -0
  72. package/dist/package/declarations/src/module.js +9 -0
  73. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_card.d.ts +145 -0
  74. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_card.js +6 -0
  75. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_dashboard.d.ts +107 -0
  76. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_dashboard.js +6 -0
  77. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_query.d.ts +157 -0
  78. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_query.js +6 -0
  79. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_query_record.d.ts +83 -0
  80. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_query_record.js +15 -0
  81. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_source.d.ts +301 -0
  82. package/dist/package/declarations/src/rpc/bi/admin/v1/bi_source.js +6 -0
  83. package/dist/package/declarations/src/rpc/common/v1/types.d.ts +60 -0
  84. package/dist/package/declarations/src/rpc/common/v1/types.js +6 -0
  85. package/dist/package/declarations/src/schema/layout.d.ts +7 -0
  86. package/dist/package/declarations/src/schema/layout.js +52 -0
  87. package/dist/package/declarations/src/schema/types.d.ts +83 -0
  88. package/dist/package/declarations/src/schema/types.js +1 -0
  89. package/dist/package/declarations/src/theme/index.d.ts +29 -0
  90. package/dist/package/declarations/src/theme/index.js +83 -0
  91. package/dist/package/declarations/src/theme/palettes.d.ts +7 -0
  92. package/dist/package/declarations/src/theme/palettes.js +28 -0
  93. package/dist/package/declarations/src/theme/presets.d.ts +73 -0
  94. package/dist/package/declarations/src/theme/presets.js +235 -0
  95. package/dist/package/declarations/src/views/card/index.vue.d.ts +3 -0
  96. package/dist/package/declarations/src/views/card/index.vue.js +18 -0
  97. package/dist/package/declarations/src/views/dashboard/index.vue.d.ts +3 -0
  98. package/dist/package/declarations/src/views/dashboard/index.vue.js +107 -0
  99. package/dist/package/declarations/src/views/designer/index.vue.d.ts +3 -0
  100. package/dist/package/declarations/src/views/designer/index.vue.js +332 -0
  101. package/dist/package/declarations/src/views/manage/index.vue.d.ts +3 -0
  102. package/dist/package/declarations/src/views/manage/index.vue.js +325 -0
  103. package/dist/package/declarations/src/views/query/index.vue.d.ts +3 -0
  104. package/dist/package/declarations/src/views/query/index.vue.js +876 -0
  105. package/dist/package/declarations/src/views/source/index.vue.d.ts +3 -0
  106. package/dist/package/declarations/src/views/source/index.vue.js +343 -0
  107. package/dist/package/declarations/src/views/workbench/index.vue.d.ts +3 -0
  108. package/dist/package/declarations/src/views/workbench/index.vue.js +362 -0
  109. package/dist/package/src/api/bi/admin/v1/bi_card.ts +67 -0
  110. package/dist/package/src/api/bi/admin/v1/bi_dashboard.ts +51 -0
  111. package/dist/package/src/api/bi/admin/v1/bi_query.ts +55 -0
  112. package/dist/package/src/api/bi/admin/v1/bi_query_record.ts +25 -0
  113. package/dist/package/src/api/bi/admin/v1/bi_source.ts +80 -0
  114. package/dist/package/src/bi-core.ts +43 -0
  115. package/dist/package/src/bi-dashboard-schema.ts +69 -0
  116. package/dist/package/src/bi-messages.ts +249 -0
  117. package/dist/package/src/bi-response.ts +39 -0
  118. package/dist/package/src/bi-vue.ts +13 -0
  119. package/dist/package/src/card-schema.ts +111 -0
  120. package/dist/package/src/components/BiCardChartEditor.vue +139 -0
  121. package/dist/package/src/components/BiCardFormDialog.vue +289 -0
  122. package/dist/package/src/components/BiCardLibrary.vue +310 -0
  123. package/dist/package/src/components/BiCardVisualization.vue +95 -0
  124. package/dist/package/src/components/BiChart.vue +134 -0
  125. package/dist/package/src/components/BiDashboard.vue +53 -0
  126. package/dist/package/src/components/BiDashboardMenuDialog.vue +177 -0
  127. package/dist/package/src/components/BiDataCard.vue +231 -0
  128. package/dist/package/src/components/BiDesigner.vue +287 -0
  129. package/dist/package/src/components/BiSourceModelDialog.vue +846 -0
  130. package/dist/package/src/components/BiWorkbenchPreview.vue +131 -0
  131. package/dist/package/src/components/source-model-layout.ts +62 -0
  132. package/dist/package/src/dataset/index.ts +28 -0
  133. package/dist/package/src/dataset/labels.ts +19 -0
  134. package/dist/package/src/dataset/transform.ts +189 -0
  135. package/dist/package/src/engine/chart-types.ts +44 -0
  136. package/dist/package/src/engine/echarts.ts +568 -0
  137. package/dist/package/src/engine/types.ts +60 -0
  138. package/dist/package/src/env.ts +82 -0
  139. package/dist/package/src/index.ts +1 -0
  140. package/dist/package/src/locales/en-US.json +254 -0
  141. package/dist/package/src/locales/generated.ts +16 -0
  142. package/dist/package/src/locales/ja-JP.json +254 -0
  143. package/dist/package/src/locales/zh-CN.json +254 -0
  144. package/dist/package/src/locales/zh-TW.json +254 -0
  145. package/dist/package/src/module.ts +12 -0
  146. package/dist/package/src/rpc/bi/admin/v1/bi_card.ts +169 -0
  147. package/dist/package/src/rpc/bi/admin/v1/bi_dashboard.ts +126 -0
  148. package/dist/package/src/rpc/bi/admin/v1/bi_query.ts +187 -0
  149. package/dist/package/src/rpc/bi/admin/v1/bi_query_record.ts +98 -0
  150. package/dist/package/src/rpc/bi/admin/v1/bi_source.ts +338 -0
  151. package/dist/package/src/rpc/common/v1/types.ts +77 -0
  152. package/dist/package/src/schema/layout.ts +54 -0
  153. package/dist/package/src/schema/types.ts +103 -0
  154. package/dist/package/src/styles.scss +1 -0
  155. package/dist/package/src/theme/index.ts +108 -0
  156. package/dist/package/src/theme/palettes.ts +34 -0
  157. package/dist/package/src/theme/presets.ts +278 -0
  158. package/dist/package/src/views/card/index.vue +5 -0
  159. package/dist/package/src/views/dashboard/index.vue +58 -0
  160. package/dist/package/src/views/designer/index.vue +164 -0
  161. package/dist/package/src/views/manage/index.vue +276 -0
  162. package/dist/package/src/views/query/index.vue +593 -0
  163. package/dist/package/src/views/source/index.vue +214 -0
  164. package/dist/package/src/views/workbench/index.vue +208 -0
  165. package/package.json +67 -3
  166. 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>