@vela-studio/ui 1.0.1
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 +152 -0
- package/dist/index.d.ts +696 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +11786 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.js +10 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/style.css +1 -0
- package/index.ts +150 -0
- package/package.json +73 -0
- package/src/components/advanced/scripting/Scripting.vue +189 -0
- package/src/components/advanced/state/State.vue +231 -0
- package/src/components/advanced/trigger/Trigger.vue +256 -0
- package/src/components/basic/button/Button.vue +120 -0
- package/src/components/basic/container/Container.vue +22 -0
- package/src/components/chart/barChart/barChart.vue +176 -0
- package/src/components/chart/doughnutChart/doughnutChart.vue +128 -0
- package/src/components/chart/funnelChart/funnelChart.vue +128 -0
- package/src/components/chart/gaugeChart/gaugeChart.vue +144 -0
- package/src/components/chart/lineChart/lineChart.vue +188 -0
- package/src/components/chart/pieChart/pieChart.vue +114 -0
- package/src/components/chart/radarChart/radarChart.vue +115 -0
- package/src/components/chart/sankeyChart/sankeyChart.vue +144 -0
- package/src/components/chart/scatterChart/scatterChart.vue +162 -0
- package/src/components/chart/stackedBarChart/stackedBarChart.vue +184 -0
- package/src/components/content/html/Html.vue +104 -0
- package/src/components/content/iframe/Iframe.vue +111 -0
- package/src/components/content/markdown/Markdown.vue +174 -0
- package/src/components/controls/breadcrumb/Breadcrumb.vue +79 -0
- package/src/components/controls/buttonGroup/ButtonGroup.vue +93 -0
- package/src/components/controls/checkboxGroup/CheckboxGroup.vue +147 -0
- package/src/components/controls/dateRange/DateRange.vue +174 -0
- package/src/components/controls/multiSelect/MultiSelect.vue +155 -0
- package/src/components/controls/navButton/NavButton.vue +97 -0
- package/src/components/controls/pagination/Pagination.vue +94 -0
- package/src/components/controls/searchBox/SearchBox.vue +170 -0
- package/src/components/controls/select/Select.vue +134 -0
- package/src/components/controls/slider/Slider.vue +167 -0
- package/src/components/controls/switch/Switch.vue +107 -0
- package/src/components/data/cardGrid/CardGrid.vue +318 -0
- package/src/components/data/list/List.vue +282 -0
- package/src/components/data/pivot/Pivot.vue +270 -0
- package/src/components/data/table/Table.vue +150 -0
- package/src/components/data/timeline/Timeline.vue +315 -0
- package/src/components/group/Group.vue +75 -0
- package/src/components/kpi/box/Box.vue +98 -0
- package/src/components/kpi/countUp/CountUp.vue +193 -0
- package/src/components/kpi/progress/Progress.vue +159 -0
- package/src/components/kpi/stat/Stat.vue +205 -0
- package/src/components/kpi/text/Text.vue +74 -0
- package/src/components/layout/badge/Badge.vue +105 -0
- package/src/components/layout/col/Col.vue +114 -0
- package/src/components/layout/flex/Flex.vue +105 -0
- package/src/components/layout/grid/Grid.vue +89 -0
- package/src/components/layout/modal/Modal.vue +118 -0
- package/src/components/layout/panel/Panel.vue +162 -0
- package/src/components/layout/row/Row.vue +99 -0
- package/src/components/layout/tabs/Tabs.vue +117 -0
- package/src/components/media/image/Image.vue +132 -0
- package/src/components/media/video/Video.vue +115 -0
- package/src/components/v2/basic/BaseButton.vue +179 -0
- package/src/components/v2/kpi/KpiCard.vue +215 -0
- package/src/components/v2/layout/GridBox.vue +55 -0
- package/src/hooks/useDataSource.ts +123 -0
- package/src/types/gis.ts +251 -0
- package/src/utils/chartUtils.ts +349 -0
- package/src/utils/dataUtils.ts +403 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { ref, watch, onUnmounted, type Ref } from 'vue'
|
|
2
|
+
import axios, { type AxiosRequestConfig } from 'axios'
|
|
3
|
+
|
|
4
|
+
export interface DataSource {
|
|
5
|
+
enabled?: boolean
|
|
6
|
+
url?: string
|
|
7
|
+
method?: string
|
|
8
|
+
headers?: Record<string, string>
|
|
9
|
+
body?: string
|
|
10
|
+
interval?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 数据源 Hook
|
|
15
|
+
* 支持 HTTP 请求和自动刷新
|
|
16
|
+
*
|
|
17
|
+
* 职责:仅负责网络请求,返回完整响应数据
|
|
18
|
+
* 数据提取:由各组件使用 dataUtils 工具函数自行提取所需字段
|
|
19
|
+
* 优势:组件可灵活提取多个字段,互不影响
|
|
20
|
+
*/
|
|
21
|
+
export function useDataSource(dataSource: Ref<DataSource | undefined>) {
|
|
22
|
+
const data = ref<unknown>(null)
|
|
23
|
+
const rawData = ref<unknown>(null) // 完整响应数据
|
|
24
|
+
const loading = ref(false)
|
|
25
|
+
const error = ref<string | null>(null)
|
|
26
|
+
let intervalId: number | null = null
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 执行数据请求
|
|
30
|
+
*/
|
|
31
|
+
const fetchData = async () => {
|
|
32
|
+
const ds = dataSource.value
|
|
33
|
+
if (!ds || !ds.enabled || !ds.url) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
loading.value = true
|
|
38
|
+
error.value = null
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// 构建请求配置
|
|
42
|
+
const config: AxiosRequestConfig = {
|
|
43
|
+
method: ds.method || 'GET',
|
|
44
|
+
url: ds.url,
|
|
45
|
+
headers: ds.headers || {},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 处理请求体(仅 POST/PUT/DELETE)
|
|
49
|
+
if (ds.method && ['POST', 'PUT', 'DELETE'].includes(ds.method) && ds.body) {
|
|
50
|
+
try {
|
|
51
|
+
config.data = JSON.parse(ds.body)
|
|
52
|
+
} catch {
|
|
53
|
+
error.value = '请求体 JSON 格式错误'
|
|
54
|
+
loading.value = false
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 发送请求
|
|
60
|
+
const response = await axios(config)
|
|
61
|
+
|
|
62
|
+
// 保存完整响应数据
|
|
63
|
+
rawData.value = response.data
|
|
64
|
+
data.value = response.data
|
|
65
|
+
error.value = null
|
|
66
|
+
} catch (err: unknown) {
|
|
67
|
+
error.value = (err as Error).message || '请求失败'
|
|
68
|
+
console.error('Data source fetch error:', err)
|
|
69
|
+
} finally {
|
|
70
|
+
loading.value = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 启动定时刷新
|
|
76
|
+
*/
|
|
77
|
+
const startPolling = () => {
|
|
78
|
+
stopPolling()
|
|
79
|
+
const ds = dataSource.value
|
|
80
|
+
if (ds && ds.enabled && ds.interval && ds.interval > 0) {
|
|
81
|
+
// interval 单位为秒,转换为毫秒
|
|
82
|
+
intervalId = window.setInterval(fetchData, ds.interval * 1000)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 停止定时刷新
|
|
88
|
+
*/
|
|
89
|
+
const stopPolling = () => {
|
|
90
|
+
if (intervalId !== null) {
|
|
91
|
+
clearInterval(intervalId)
|
|
92
|
+
intervalId = null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 监听 dataSource 变化
|
|
97
|
+
watch(
|
|
98
|
+
() => dataSource.value,
|
|
99
|
+
(newDs) => {
|
|
100
|
+
stopPolling()
|
|
101
|
+
if (newDs && newDs.enabled && newDs.url) {
|
|
102
|
+
fetchData()
|
|
103
|
+
startPolling()
|
|
104
|
+
} else {
|
|
105
|
+
data.value = null
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{ immediate: true, deep: true },
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// 组件卸载时清理定时器
|
|
112
|
+
onUnmounted(() => {
|
|
113
|
+
stopPolling()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
data,
|
|
118
|
+
rawData, // 完整响应数据
|
|
119
|
+
loading,
|
|
120
|
+
error,
|
|
121
|
+
fetchData, // 手动刷新
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/types/gis.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GIS 通用类型定义
|
|
3
|
+
* Smart 和 Dumb 组件之间的数据契约
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ==================== 基础类型 ====================
|
|
7
|
+
|
|
8
|
+
/** 基础坐标点 */
|
|
9
|
+
export interface GISPoint {
|
|
10
|
+
lat: number
|
|
11
|
+
lng: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 带强度的热力点 */
|
|
15
|
+
export interface HeatPoint extends GISPoint {
|
|
16
|
+
/** 热力强度 0-1 */
|
|
17
|
+
intensity?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 聚合/标记点数据 */
|
|
21
|
+
export interface MarkerPoint extends GISPoint {
|
|
22
|
+
/** 唯一标识 */
|
|
23
|
+
id?: string | number
|
|
24
|
+
/** 标签文本 */
|
|
25
|
+
label?: string
|
|
26
|
+
/** 弹窗内容 */
|
|
27
|
+
popup?: string
|
|
28
|
+
/** 图标 URL */
|
|
29
|
+
icon?: string
|
|
30
|
+
/** 图标大小 [width, height] */
|
|
31
|
+
iconSize?: [number, number]
|
|
32
|
+
/** 图标锚点 [x, y] */
|
|
33
|
+
iconAnchor?: [number, number]
|
|
34
|
+
/** 自定义数据 */
|
|
35
|
+
data?: Record<string, unknown>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ==================== GeoJSON 类型 ====================
|
|
39
|
+
|
|
40
|
+
/** GeoJSON 几何类型 */
|
|
41
|
+
export type GeoJSONGeometryType =
|
|
42
|
+
| 'Point'
|
|
43
|
+
| 'MultiPoint'
|
|
44
|
+
| 'LineString'
|
|
45
|
+
| 'MultiLineString'
|
|
46
|
+
| 'Polygon'
|
|
47
|
+
| 'MultiPolygon'
|
|
48
|
+
| 'GeometryCollection'
|
|
49
|
+
|
|
50
|
+
/** GeoJSON 几何对象 */
|
|
51
|
+
export interface GeoJSONGeometry {
|
|
52
|
+
type: GeoJSONGeometryType
|
|
53
|
+
coordinates: number[] | number[][] | number[][][] | number[][][][]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** GeoJSON Feature */
|
|
57
|
+
export interface GeoJSONFeature<P = Record<string, unknown>> {
|
|
58
|
+
type: 'Feature'
|
|
59
|
+
geometry: GeoJSONGeometry
|
|
60
|
+
properties: P
|
|
61
|
+
id?: string | number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** GeoJSON FeatureCollection */
|
|
65
|
+
export interface GeoJSONFeatureCollection<P = Record<string, unknown>> {
|
|
66
|
+
type: 'FeatureCollection'
|
|
67
|
+
features: GeoJSONFeature<P>[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** GeoJSON 数据类型联合 */
|
|
71
|
+
export type GeoJSONData<P = Record<string, unknown>> =
|
|
72
|
+
| GeoJSONFeature<P>
|
|
73
|
+
| GeoJSONFeatureCollection<P>
|
|
74
|
+
|
|
75
|
+
// ==================== 地图配置类型 ====================
|
|
76
|
+
|
|
77
|
+
/** 边界框 */
|
|
78
|
+
export interface MapBounds {
|
|
79
|
+
northEast: GISPoint
|
|
80
|
+
southWest: GISPoint
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 地图视口配置 */
|
|
84
|
+
export interface MapViewport {
|
|
85
|
+
center: GISPoint
|
|
86
|
+
zoom: number
|
|
87
|
+
bounds?: MapBounds
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 瓦片图层配置 */
|
|
91
|
+
export interface TileLayerConfig {
|
|
92
|
+
url: string
|
|
93
|
+
attribution?: string
|
|
94
|
+
minZoom?: number
|
|
95
|
+
maxZoom?: number
|
|
96
|
+
subdomains?: string[]
|
|
97
|
+
opacity?: number
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 图层可见性配置 */
|
|
101
|
+
export interface LayerVisibility {
|
|
102
|
+
id: string
|
|
103
|
+
name: string
|
|
104
|
+
visible: boolean
|
|
105
|
+
type: 'tile' | 'heat' | 'cluster' | 'geojson' | 'vector'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ==================== 热力图配置类型 ====================
|
|
109
|
+
|
|
110
|
+
/** 热力图渐变色配置 */
|
|
111
|
+
export interface HeatGradient {
|
|
112
|
+
[stop: number]: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 热力图配置 */
|
|
116
|
+
export interface HeatLayerConfig {
|
|
117
|
+
/** 热力点半径(像素) */
|
|
118
|
+
radius?: number
|
|
119
|
+
/** 模糊程度(像素) */
|
|
120
|
+
blur?: number
|
|
121
|
+
/** 最大缩放级别 */
|
|
122
|
+
maxZoom?: number
|
|
123
|
+
/** 最大强度值 */
|
|
124
|
+
max?: number
|
|
125
|
+
/** 最小透明度 */
|
|
126
|
+
minOpacity?: number
|
|
127
|
+
/** 渐变色配置 */
|
|
128
|
+
gradient?: HeatGradient
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ==================== 聚合图配置类型 ====================
|
|
132
|
+
|
|
133
|
+
/** 聚合图标配置 */
|
|
134
|
+
export interface ClusterIconConfig {
|
|
135
|
+
/** 默认图标 URL */
|
|
136
|
+
iconUrl?: string
|
|
137
|
+
/** 图标大小 */
|
|
138
|
+
iconSize?: [number, number]
|
|
139
|
+
/** 图标锚点 */
|
|
140
|
+
iconAnchor?: [number, number]
|
|
141
|
+
/** 弹窗锚点 */
|
|
142
|
+
popupAnchor?: [number, number]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 聚合簇样式配置 */
|
|
146
|
+
export interface ClusterStyleConfig {
|
|
147
|
+
/** 小聚合簇阈值 */
|
|
148
|
+
smallThreshold?: number
|
|
149
|
+
/** 中聚合簇阈值 */
|
|
150
|
+
mediumThreshold?: number
|
|
151
|
+
/** 小聚合簇样式 */
|
|
152
|
+
smallStyle?: {
|
|
153
|
+
backgroundColor?: string
|
|
154
|
+
color?: string
|
|
155
|
+
size?: number
|
|
156
|
+
}
|
|
157
|
+
/** 中聚合簇样式 */
|
|
158
|
+
mediumStyle?: {
|
|
159
|
+
backgroundColor?: string
|
|
160
|
+
color?: string
|
|
161
|
+
size?: number
|
|
162
|
+
}
|
|
163
|
+
/** 大聚合簇样式 */
|
|
164
|
+
largeStyle?: {
|
|
165
|
+
backgroundColor?: string
|
|
166
|
+
color?: string
|
|
167
|
+
size?: number
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 聚合图层配置 */
|
|
172
|
+
export interface ClusterLayerConfig {
|
|
173
|
+
/** 最大聚合半径 */
|
|
174
|
+
maxClusterRadius?: number
|
|
175
|
+
/** 禁用聚合的缩放级别 */
|
|
176
|
+
disableClusteringAtZoom?: number
|
|
177
|
+
/** 最大缩放时展开为蜘蛛网形式 */
|
|
178
|
+
spiderfyOnMaxZoom?: boolean
|
|
179
|
+
/** 鼠标悬停时显示聚合范围 */
|
|
180
|
+
showCoverageOnHover?: boolean
|
|
181
|
+
/** 点击聚合簇时缩放到边界 */
|
|
182
|
+
zoomToBoundsOnClick?: boolean
|
|
183
|
+
/** 是否启用分块加载 */
|
|
184
|
+
chunkedLoading?: boolean
|
|
185
|
+
/** 图标配置 */
|
|
186
|
+
iconConfig?: ClusterIconConfig
|
|
187
|
+
/** 聚合簇样式 */
|
|
188
|
+
clusterStyle?: ClusterStyleConfig
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ==================== 事件类型 ====================
|
|
192
|
+
|
|
193
|
+
/** 地图点击事件数据 */
|
|
194
|
+
export interface MapClickEvent {
|
|
195
|
+
latlng: GISPoint
|
|
196
|
+
layerPoint: { x: number; y: number }
|
|
197
|
+
containerPoint: { x: number; y: number }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 标记点击事件数据 */
|
|
201
|
+
export interface MarkerClickEvent {
|
|
202
|
+
marker: MarkerPoint
|
|
203
|
+
index: number
|
|
204
|
+
originalEvent?: MouseEvent
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 聚合簇点击事件数据 */
|
|
208
|
+
export interface ClusterClickEvent {
|
|
209
|
+
/** 聚合簇中心点 */
|
|
210
|
+
center: GISPoint
|
|
211
|
+
/** 聚合簇包含的标记数量 */
|
|
212
|
+
count: number
|
|
213
|
+
/** 聚合簇边界 */
|
|
214
|
+
bounds: MapBounds
|
|
215
|
+
/** 聚合簇内的所有标记 */
|
|
216
|
+
markers: MarkerPoint[]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ==================== 数据状态类型 ====================
|
|
220
|
+
|
|
221
|
+
/** 数据加载状态 */
|
|
222
|
+
export type DataLoadingState = 'idle' | 'loading' | 'success' | 'error'
|
|
223
|
+
|
|
224
|
+
/** 带状态的数据包装器 */
|
|
225
|
+
export interface DataWithState<T> {
|
|
226
|
+
data: T
|
|
227
|
+
state: DataLoadingState
|
|
228
|
+
error?: string
|
|
229
|
+
/** 数据版本号,用于增量更新检测 */
|
|
230
|
+
version?: number
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ==================== 工具类型 ====================
|
|
234
|
+
|
|
235
|
+
/** 将普通数组转换为 HeatPoint 数组的映射配置 */
|
|
236
|
+
export interface HeatDataMapping {
|
|
237
|
+
latField: string
|
|
238
|
+
lngField: string
|
|
239
|
+
intensityField?: string
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 将普通数组转换为 MarkerPoint 数组的映射配置 */
|
|
243
|
+
export interface MarkerDataMapping {
|
|
244
|
+
idField?: string
|
|
245
|
+
latField: string
|
|
246
|
+
lngField: string
|
|
247
|
+
labelField?: string
|
|
248
|
+
popupField?: string
|
|
249
|
+
iconField?: string
|
|
250
|
+
dataFields?: string[]
|
|
251
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据提取工具函数(通用)
|
|
3
|
+
* 用于所有组件的数据源、路径解析、数据格式化等
|
|
4
|
+
* 图表、KPI、Text 等组件都使用这些工具函数提取数据
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 从对象中根据路径提取值
|
|
9
|
+
* 支持点号路径和数组索引,如: "data.chart.values" 或 "items[0].name"
|
|
10
|
+
*
|
|
11
|
+
* @param obj - 源数据对象
|
|
12
|
+
* @param path - 提取路径,可选(为空则返回 undefined)
|
|
13
|
+
* @returns 提取的值,或 undefined
|
|
14
|
+
*/
|
|
15
|
+
export function getValueByPath(obj: unknown, path: string | undefined): unknown {
|
|
16
|
+
if (!path || !obj) return undefined
|
|
17
|
+
try {
|
|
18
|
+
const keys = path.replace(/\[(\d+)\]/g, '.$1').split('.')
|
|
19
|
+
let result: unknown = obj
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
if (result === null || result === undefined) return undefined
|
|
22
|
+
result = (result as Record<string, unknown>)[key]
|
|
23
|
+
}
|
|
24
|
+
return result
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 从数据源提取单个数值
|
|
32
|
+
* 用于 KPI 组件(countUp, progress, badge 等)
|
|
33
|
+
*
|
|
34
|
+
* @param remoteData - 远程数据对象
|
|
35
|
+
* @param valuePath - 数值路径,可选
|
|
36
|
+
* @param defaultValue - 默认值
|
|
37
|
+
* @returns 提取的数值
|
|
38
|
+
*/
|
|
39
|
+
export function extractNumber(
|
|
40
|
+
remoteData: unknown,
|
|
41
|
+
valuePath: string | undefined,
|
|
42
|
+
defaultValue: number = 0,
|
|
43
|
+
): number {
|
|
44
|
+
if (!valuePath) return defaultValue
|
|
45
|
+
|
|
46
|
+
const value = getValueByPath(remoteData, valuePath)
|
|
47
|
+
if (typeof value === 'number') return value
|
|
48
|
+
if (value !== undefined && value !== null) {
|
|
49
|
+
const parsed = parseFloat(String(value))
|
|
50
|
+
return isNaN(parsed) ? defaultValue : parsed
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return defaultValue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 解析逗号分隔的数字输入
|
|
58
|
+
* 支持 JSON 数组格式和逗号分隔格式
|
|
59
|
+
* @param input - 输入字符串,如 "150, 230, 224" 或 "[150, 230, 224]"
|
|
60
|
+
* @param defaultValue - 默认值数组
|
|
61
|
+
*/
|
|
62
|
+
export function parseNumberInput(input: string | undefined, defaultValue: number[] = []): number[] {
|
|
63
|
+
if (!input) return defaultValue
|
|
64
|
+
|
|
65
|
+
// 尝试 JSON 解析
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(input)
|
|
68
|
+
if (Array.isArray(parsed)) {
|
|
69
|
+
return parsed
|
|
70
|
+
.map((v) => (typeof v === 'number' ? v : parseFloat(String(v))))
|
|
71
|
+
.filter((v) => !isNaN(v))
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// JSON 解析失败,尝试逗号分隔
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 逗号分隔解析
|
|
78
|
+
return input
|
|
79
|
+
.split(',')
|
|
80
|
+
.map((v) => parseFloat(v.trim()))
|
|
81
|
+
.filter((v) => !isNaN(v))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 解析逗号分隔的字符串输入
|
|
86
|
+
* 支持 JSON 数组格式和逗号分隔格式
|
|
87
|
+
* @param input - 输入字符串,如 "Mon, Tue, Wed" 或 '["Mon", "Tue", "Wed"]'
|
|
88
|
+
* @param defaultValue - 默认值数组
|
|
89
|
+
*/
|
|
90
|
+
export function parseStringInput(input: string | undefined, defaultValue: string[] = []): string[] {
|
|
91
|
+
if (!input) return defaultValue
|
|
92
|
+
|
|
93
|
+
// 尝试 JSON 解析
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(input)
|
|
96
|
+
if (Array.isArray(parsed)) {
|
|
97
|
+
return parsed.map((v) => String(v))
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// JSON 解析失败,尝试逗号分隔
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 逗号分隔解析
|
|
104
|
+
return input
|
|
105
|
+
.split(',')
|
|
106
|
+
.map((v) => v.trim())
|
|
107
|
+
.filter((v) => v.length > 0)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 解析二维数组输入(用于散点图等)
|
|
112
|
+
* @param input - 输入字符串,如 "[[10, 8], [8, 7]]"
|
|
113
|
+
* @param defaultValue - 默认值数组
|
|
114
|
+
*/
|
|
115
|
+
export function parse2DArrayInput(
|
|
116
|
+
input: string | undefined,
|
|
117
|
+
defaultValue: Array<[number, number]> = [],
|
|
118
|
+
): Array<[number, number]> {
|
|
119
|
+
if (!input) return defaultValue
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(input)
|
|
123
|
+
if (Array.isArray(parsed) && parsed.length > 0 && Array.isArray(parsed[0])) {
|
|
124
|
+
return parsed.map((item) => {
|
|
125
|
+
if (Array.isArray(item) && item.length >= 2) {
|
|
126
|
+
return [
|
|
127
|
+
typeof item[0] === 'number' ? item[0] : parseFloat(String(item[0])),
|
|
128
|
+
typeof item[1] === 'number' ? item[1] : parseFloat(String(item[1])),
|
|
129
|
+
] as [number, number]
|
|
130
|
+
}
|
|
131
|
+
return [0, 0] as [number, number]
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// JSON 解析失败
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return defaultValue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 从数据源提取数字数组
|
|
143
|
+
* @param remoteData - 远程数据对象
|
|
144
|
+
* @param dataPath - 数据路径
|
|
145
|
+
*/
|
|
146
|
+
export function extractNumberArray(
|
|
147
|
+
remoteData: unknown,
|
|
148
|
+
dataPath: string | undefined,
|
|
149
|
+
): number[] | undefined {
|
|
150
|
+
if (!dataPath) return undefined
|
|
151
|
+
|
|
152
|
+
const extractedData = getValueByPath(remoteData, dataPath)
|
|
153
|
+
if (Array.isArray(extractedData)) {
|
|
154
|
+
return extractedData.map((v) => (typeof v === 'number' ? v : parseFloat(String(v))))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 从数据源提取字符串数组
|
|
162
|
+
* @param remoteData - 远程数据对象
|
|
163
|
+
* @param dataPath - 数据路径
|
|
164
|
+
*/
|
|
165
|
+
export function extractStringArray(
|
|
166
|
+
remoteData: unknown,
|
|
167
|
+
dataPath: string | undefined,
|
|
168
|
+
): string[] | undefined {
|
|
169
|
+
if (!dataPath) return undefined
|
|
170
|
+
|
|
171
|
+
const extractedData = getValueByPath(remoteData, dataPath)
|
|
172
|
+
if (Array.isArray(extractedData)) {
|
|
173
|
+
return extractedData.map((v) => String(v))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 从数据源提取二维数组(用于散点图等)
|
|
181
|
+
* @param remoteData - 远程数据对象
|
|
182
|
+
* @param dataPath - 数据路径
|
|
183
|
+
*/
|
|
184
|
+
export function extract2DArray(
|
|
185
|
+
remoteData: unknown,
|
|
186
|
+
dataPath: string | undefined,
|
|
187
|
+
): Array<[number, number]> | undefined {
|
|
188
|
+
if (!dataPath) return undefined
|
|
189
|
+
|
|
190
|
+
const extractedData = getValueByPath(remoteData, dataPath)
|
|
191
|
+
if (Array.isArray(extractedData) && extractedData.length > 0 && Array.isArray(extractedData[0])) {
|
|
192
|
+
return extractedData.map((item) => {
|
|
193
|
+
if (Array.isArray(item) && item.length >= 2) {
|
|
194
|
+
return [
|
|
195
|
+
typeof item[0] === 'number' ? item[0] : parseFloat(String(item[0])),
|
|
196
|
+
typeof item[1] === 'number' ? item[1] : parseFloat(String(item[1])),
|
|
197
|
+
] as [number, number]
|
|
198
|
+
}
|
|
199
|
+
return [0, 0] as [number, number]
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 从数据源提取字符串值
|
|
208
|
+
* @param remoteData - 远程数据对象
|
|
209
|
+
* @param dataPath - 数据路径
|
|
210
|
+
*/
|
|
211
|
+
export function extractString(
|
|
212
|
+
remoteData: unknown,
|
|
213
|
+
dataPath: string | undefined,
|
|
214
|
+
): string | undefined {
|
|
215
|
+
if (!dataPath) return undefined
|
|
216
|
+
|
|
217
|
+
const extractedData = getValueByPath(remoteData, dataPath)
|
|
218
|
+
return extractedData ? String(extractedData) : undefined
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 解析 JSON 数组输入(通用)
|
|
223
|
+
* @param input - JSON 字符串输入
|
|
224
|
+
* @param defaultValue - 默认值
|
|
225
|
+
*/
|
|
226
|
+
export function parseJSONInput<T = unknown>(input: string | undefined, defaultValue: T): T {
|
|
227
|
+
if (!input) return defaultValue
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const parsed = JSON.parse(input)
|
|
231
|
+
return parsed as T
|
|
232
|
+
} catch (e) {
|
|
233
|
+
console.error('Failed to parse JSON input:', e)
|
|
234
|
+
return defaultValue
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 提取并规范化 Sankey 图节点数据
|
|
240
|
+
* @param remoteData - 远程数据对象
|
|
241
|
+
* @param dataPath - 数据路径
|
|
242
|
+
*/
|
|
243
|
+
export function extractSankeyNodes(
|
|
244
|
+
remoteData: unknown,
|
|
245
|
+
dataPath: string | undefined,
|
|
246
|
+
): Array<{ name: string; value?: number; depth?: number; itemStyle?: unknown }> | undefined {
|
|
247
|
+
if (!dataPath) return undefined
|
|
248
|
+
|
|
249
|
+
const nodesData = getValueByPath(remoteData, dataPath)
|
|
250
|
+
if (!Array.isArray(nodesData) || nodesData.length === 0) return undefined
|
|
251
|
+
|
|
252
|
+
return nodesData.map((node: unknown) => {
|
|
253
|
+
const obj =
|
|
254
|
+
typeof node === 'object' && node !== null ? (node as Record<string, unknown>) : undefined
|
|
255
|
+
const name = typeof node === 'string' ? node : String(obj?.name ?? obj?.id ?? '')
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
name,
|
|
259
|
+
value:
|
|
260
|
+
obj && typeof obj.value === 'number'
|
|
261
|
+
? (obj.value as number)
|
|
262
|
+
: obj && obj.value != null
|
|
263
|
+
? parseFloat(String(obj.value))
|
|
264
|
+
: undefined,
|
|
265
|
+
depth:
|
|
266
|
+
obj && typeof obj.depth === 'number'
|
|
267
|
+
? (obj.depth as number)
|
|
268
|
+
: obj && obj.depth != null
|
|
269
|
+
? parseFloat(String(obj.depth))
|
|
270
|
+
: undefined,
|
|
271
|
+
itemStyle: obj ? obj.itemStyle : undefined,
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 提取并规范化 Sankey 图连接数据
|
|
278
|
+
* @param remoteData - 远程数据对象
|
|
279
|
+
* @param dataPath - 数据路径
|
|
280
|
+
*/
|
|
281
|
+
export function extractSankeyLinks(
|
|
282
|
+
remoteData: unknown,
|
|
283
|
+
dataPath: string | undefined,
|
|
284
|
+
): Array<{ source: string; target: string; value: number }> | undefined {
|
|
285
|
+
if (!dataPath) return undefined
|
|
286
|
+
|
|
287
|
+
const linksData = getValueByPath(remoteData, dataPath)
|
|
288
|
+
if (!Array.isArray(linksData) || linksData.length === 0) return undefined
|
|
289
|
+
|
|
290
|
+
return linksData.map((link: unknown) => {
|
|
291
|
+
const l = link as Record<string, unknown>
|
|
292
|
+
return {
|
|
293
|
+
source: (l.source || l.from || '') as string,
|
|
294
|
+
target: (l.target || l.to || '') as string,
|
|
295
|
+
value: (l.value || 0) as number,
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* KPI 组件专用:提取多个字段的数据
|
|
302
|
+
* 用于 stat 组件等需要同时提取多个字段的场景
|
|
303
|
+
*
|
|
304
|
+
* @param remoteData - 远程数据对象
|
|
305
|
+
* @param paths - 字段路径映射 { title: 'data.title', value: 'data.value', ... }
|
|
306
|
+
* @returns 提取的数据对象
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* const data = extractMultipleFields(remoteData, {
|
|
310
|
+
* title: 'data.kpi.title',
|
|
311
|
+
* value: 'data.kpi.value',
|
|
312
|
+
* change: 'data.kpi.change'
|
|
313
|
+
* })
|
|
314
|
+
* // 返回: { title: '销售额', value: 12345, change: 5.2 }
|
|
315
|
+
*/
|
|
316
|
+
export function extractMultipleFields<T extends Record<string, string | undefined>>(
|
|
317
|
+
remoteData: unknown,
|
|
318
|
+
paths: T,
|
|
319
|
+
): Record<keyof T, unknown> {
|
|
320
|
+
const result: Record<string, unknown> = {}
|
|
321
|
+
|
|
322
|
+
for (const [key, path] of Object.entries(paths)) {
|
|
323
|
+
if (path && typeof path === 'string') {
|
|
324
|
+
result[key] = getValueByPath(remoteData, path)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return result as Record<keyof T, unknown>
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 智能提取数据:优先使用路径提取,否则使用默认值
|
|
333
|
+
* 用于所有简单组件(text, countUp, progress, badge 等)
|
|
334
|
+
*
|
|
335
|
+
* @param remoteData - 远程数据对象
|
|
336
|
+
* @param path - 数据路径
|
|
337
|
+
* @param fallbackValue - 回退值(当路径未配置或提取失败时使用)
|
|
338
|
+
* @returns 提取的数据或回退值
|
|
339
|
+
*/
|
|
340
|
+
export function extractWithFallback<T>(
|
|
341
|
+
remoteData: unknown,
|
|
342
|
+
path: string | undefined,
|
|
343
|
+
fallbackValue: T,
|
|
344
|
+
): T {
|
|
345
|
+
if (!path) return fallbackValue
|
|
346
|
+
|
|
347
|
+
const extracted = getValueByPath(remoteData, path)
|
|
348
|
+
return (extracted ?? fallbackValue) as T
|
|
349
|
+
}
|